From 2aaa353f7afe019121a4c884d29e492641045fcc Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Thu, 6 Jul 2023 01:13:37 +0200 Subject: [PATCH 001/100] EX: Initialize script for PET vs. simulation example included in DarSIA paper. --- examples/paper/pet_simulations_comparison.py | 716 +++++++++++++++++++ 1 file changed, 716 insertions(+) create mode 100644 examples/paper/pet_simulations_comparison.py diff --git a/examples/paper/pet_simulations_comparison.py b/examples/paper/pet_simulations_comparison.py new file mode 100644 index 00000000..74686c19 --- /dev/null +++ b/examples/paper/pet_simulations_comparison.py @@ -0,0 +1,716 @@ +"""Comparisons of block b closed for dicom and vtu images. + +""" + +import time +from pathlib import Path + +import matplotlib.pyplot as plt +import numpy as np + +import darsia + +# ! ---- Constants + +cm2m = 1e-2 # conversion cm -> m +h2s = 60**2 # conversion hours -> seconds +ml_per_hour_to_m3_per_s = cm2m**3 / h2s + +# ! ---- Model paramaters + +porosity_2d = 0.2321 +fracture_aperture = 0.1 * cm2m +depth = 1.95 * cm2m +injection_rate = 15 * ml_per_hour_to_m3_per_s + +# ! ---- Read DICOM images + + +def read_dicom_images() -> darsia.Image: + """Read space-time image from DICOM format for fractip a.""" + + # Provide folder with dicom images (note: two folders) + root = Path( + "/home/jakub/images/ift/fracflow/01-single-phase-tracer-in-fracture/fractip-b" + ) + images = [ + root / Path(p) / Path("DICOM/PA1/ST1/SE1") + for p in [ + "fractip b closed 1 min rekon start 0 15 frames", + # "fractip b closed 1 min frames start 3780", + ] + ] + + if False: + # Stack images (need to use such approach due to lack of common reference + dicom_images = [] + for img in images: + dicom_image = darsia.imread(img, suffix=".dcm", dim=3, series=True) + dicom_images.append(dicom_image) + uncorrected_image_3d = darsia.stack(dicom_images) + + # Use an assistant to tune the rotation correction and subregion selection + test_image = uncorrected_image_3d.time_slice(9) + test_image.show( + surpress_3d=True, mode="matplotlib", side_view="voxel", cmap="turbo" + ) + test_image.show( + mode="plotly", + side_view="voxel", + view="voxel", + cmap="turbo", + threshold=0.8, + relative=True, + ) + options = { + "threshold": 0.05, + "relative": True, + "verbosity": True, + } + rotation_assistant = darsia.RotationCorrectionAssistant(test_image, **options) + rotation_corrections = rotation_assistant() + rotated_test_image = test_image.copy() + for rotation in rotation_corrections: + rotated_test_image = rotation(rotated_test_image) + subregion_assistant = darsia.SubregionAssistant(rotated_test_image, **options) + coordinates = subregion_assistant() + + else: + # Alternative: Fix tailored input parameters for the correction objects. + rotation_corrections = [ + darsia.RotationCorrection( + **{ + "anchor": np.array([91, 106, 13]), + "rotation_from_isometry": True, + "pts_src": np.array([[91, 106, 13], [97, 106, 86]]), + "pts_dst": np.array([[91, 106, 13], [91, 106, 86]]), + } + ), + darsia.RotationCorrection( + **{ + "anchor": np.array([50, 96, 117]), + "rotation_from_isometry": True, + "pts_src": np.array([[50, 96, 117], [90, 97, 117]]), + "pts_dst": np.array([[50, 96, 117], [90, 96, 117]]), + } + ), + darsia.RotationCorrection( + **{ + "anchor": np.array([106, 94, 0]), + "rotation_from_isometry": True, + "pts_src": np.array([[106, 94, 0], [106, 96, 70]]), + "pts_dst": np.array([[106, 94, 0], [106, 95, 69]]), + } + ), + ] + coordinates = np.array( + [ + [42.21969627956989, 42.29322659139785, -195.13738119462366], + [42.30791713978495, 42.36046250537635, -195.1190467860215], + ] + ) + + # Re-read the dicom image. + # Use the uncorrected images further. + image_3d = darsia.imread( + images, + suffix=".dcm", + dim=3, + series=True, + ) + image_3d = image_3d.time_slice(9) + for correction in rotation_corrections: + image_3d = correction(image_3d) + image_3d = image_3d.subregion(coordinates=coordinates) + + # Plot side view to check the result + if True: + image_3d.show( + mode="matplotlib", + surpress_3d=True, + side_view="voxel", + threshold=0.05, + relative=True, + ) + + # ! ---- Precondition + image_3d.img /= np.max(image_3d.img) + + return image_3d + + +def plot_3d_image(image_3d): + # Plot side view to check the result + image_3d.show( + mode="matplotlib", + surpress_3d=True, + side_view="voxel", + threshold=0.05, + relative=True, + ) + + +def plot_sum(image_3d): + if not isinstance(image_3d, list): + image_3d = [image_3d] + max_val = max( + [ + np.max( + np.sum(img, axis=0) + if isinstance(img, np.ndarray) + else np.sum(img.img, axis=0) + ) + for img in image_3d + ] + ) + for i, img in enumerate(image_3d): + plt.figure(f"sum {i}") + if isinstance(img, np.ndarray): + plt.imshow(np.sum(img, axis=0), cmap="turbo", vmin=0, vmax=max_val) + else: + plt.imshow(np.sum(img.img, axis=0), cmap="turbo", vmin=0, vmax=max_val) + plt.colorbar() + plt.show() + + +def plot_slice(image_3d): + if not isinstance(image_3d, list): + image_3d = [image_3d] + max_val = max( + [np.max(img if isinstance(img, np.ndarray) else img.img) for img in image_3d] + ) + for i, img in enumerate(image_3d): + plt.figure(f"slice {i}") + if isinstance(img, np.ndarray): + plt.imshow(img[20], cmap="turbo", vmin=0, vmax=max_val) + else: + plt.imshow(img.img[20], cmap="turbo", vmin=0, vmax=max_val) + plt.colorbar() + plt.show() + + +# Enrich the signal under the assumption of a monotone displacement experiment. +def accumulate_signal(image: darsia.Image) -> darsia.Image: + new_image = image.copy() + for time_id in range(image.time_num): + new_image.img[..., time_id] = np.max( + image.img[..., slice(0, time_id + 1)], axis=-1 + ) + + return new_image + + +# ! ---- Apply regularization +def apply_tvd(image: darsia.Image) -> darsia.Image: + """NOTE: Most time-consuming routine.""" + + pass + + +def read_regularized_images(image: darsia.Image) -> darsia.Image: + """Read preprocessed regularized images from numpy format.""" + + reg_image = image.copy() + + for i in range(0, 13): + reg_image.img[..., i] = np.load( + # f"tvd/block-a/heterogeneous_mu_1000_iter/tvd_a_1000_{i}.npy" + # f"tvd/block-a/mu_0.0001_1000_iter/tvd_{i}.npy" # + f"tvd/delta/block-a/mu_0.0001_10000_iter/tvd_{i}.npy" # latest tvd + ) + + return reg_image + + +# ! ---- Reduce 3d image to two spatial dimensions +def dimensional_reduction(image_3d: darsia.Image) -> darsia.Image: + """Flatten 3d image.""" + return darsia.reduce_axis(image_3d, axis="z", mode="average") + + +# ! ---- Rescale images to concentrations + + +def extract_concentration( + image_2d: darsia.Image, calibration_interval, injection_rate: float +) -> darsia.Image: + # Define default concentration analysis, enabling calibration + class CalibratedConcentrationAnalysis( + darsia.ConcentrationAnalysis, darsia.InjectionRateModelObjectiveMixin + ): + pass + + model = darsia.ScalingModel(scaling=1) + dicom_concentration_analysis = CalibratedConcentrationAnalysis( + base=None, model=model + ) + + # Calibrate concentration analysis + shape_meta = image_2d.shape_metadata() + geometry = darsia.ExtrudedGeometry(expansion=depth, **shape_meta) + + dicom_concentration_analysis.calibrate_model( + images=image_2d.time_interval(calibration_interval), + options={ + "geometry": geometry, + "injection_rate": injection_rate, + "initial_guess": [1.0], + "tol": 1e-5, + "maxiter": 100, + "method": "Nelder-Mead", + "regression_type": "ransac", + }, + plot_result=True, + ) + + # Interpret the calibration results to effectively determine the injection start. + reference_time = dicom_concentration_analysis.model_calibration_postanalysis() + + # The results is that the effective injection start occurs at 3.25 minutes. Thus, update + # the reference time, or simply the relative time. Set reference time with respect to + # the first active image. Use seconds. + image_2d.update_reference_time(reference_time) + + # Print some of the specs + if True: + print( + f"The identified reference time / injection start is: {reference_time} [s]" + ) + print( + f"The dimensions of the space time dicom image are: {image_2d.dimensions}" + ) + print(f"Relative times in seconds: {image_2d.time}") + + # Only for plotting reasons - same as above but with updated times and with activated plot: + if False: + dicom_concentration_analysis.calibrate_model( + images=image_2d.time_interval( + calibration_interval + ), # Only use some of the first images due to cut-off in the geometry + options={ + "geometry": geometry, + "injection_rate": injection_rate, + "initial_guess": [1.0], + "tol": 1e-5, + "maxiter": 100, + "method": "Nelder-Mead", + "regression_type": "ransac", + }, + plot_result=True, + ) + + # Produce space time image respresentation of concentrations. + dicom_concentration = dicom_concentration_analysis(image_2d) + + # Debug: Plot the concentration profile. Note that values are larger than 1, which is + # due to the calibration based on a non-smooth signal. The expected volume has to be + # distributed among the active pixels. Pixels with strong activity thereby hold + # unphysical amounts of fluid. + if False: + dicom_concentration.show("dicom concentration", 5) + + return dicom_concentration + + +# Determine total concentrations over time +def plot_concentration_evolution(concentration_2d): + shape_meta = concentration_2d.shape_metadata() + geometry = darsia.ExtrudedGeometry(expansion=depth, **shape_meta) + concentration_values = geometry.integrate(concentration_2d) + plt.figure("Experiments - injected volume over time") + plt.plot(concentration_2d.time, concentration_values) + plt.plot( + concentration_2d.time, + [injection_rate * time for time in concentration_2d.time], + color="black", + linestyle=(0, (5, 5)), + ) + plt.xlabel("time [s]") + plt.ylabel("volume [m**3]") + plt.show() + + +# ! ---- VTU images + + +def read_vtu_images() -> darsia.Image: + """Read-in all available VTU data.""" + + # This corresponds approx. to (only temporary - not used further): + vtu_time = [420 + 240 * i for i in range(8, 9)] + + # The corresponding indices used for storing the simulation data: + vtu_indices = [str(i).zfill(6) for i in [84 + 48 * j for j in range(7, 8)]] + # vtu_indices = [str(i).zfill(6) for i in [84 + 48 * j for j in range(8, 9)]] + vtu_root = Path(".") + + # Find the corresponding files + vtu_images_2d = [ + vtu_root / Path(f"data_2_{str(ind).zfill(6)}.vtu") for ind in vtu_indices + ] + vtu_images_1d = [ + vtu_root / Path(f"data_1_{str(ind).zfill(6)}.vtu") for ind in vtu_indices + ] + + # Define vtu images as DarSIA images + vtu_image_2d = darsia.imread( + vtu_images_2d, + time=vtu_time, # relative time in minutes + key="temperature", # key to address concentration data + shape=(400, 400), # dim = 2 + vtu_dim=2, # effective dimension of vtu data + ) + + vtu_image_1d = darsia.imread( + vtu_images_1d, + time=vtu_time, # relative time in minutes + key="temperature", # key to address concentration data + shape=(1001, 51), # dim = 2 + vtu_dim=1, # effective dimension of vtu data + width=fracture_aperture, # for embedding in 2d + ) + + # Make vtu images series = False + vtu_image_2d = vtu_image_2d.time_slice(0) + vtu_image_1d = vtu_image_1d.time_slice(0) + + # PorePy/meshio cuts off coordinate values at some point... + # Correct manually - width / 2 - aperature / 2. + vtu_image_1d.origin[0] = (6.98 / 2 - 0.1 / 2) * cm2m + + # Equidimensional reconstructions. Superpose 2d and 1d images. + # And directly interpret + porosity_1d = 1.0 - porosity_2d # for volume conservation + vtu_image = darsia.superpose( + [ + darsia.weight(vtu_image_2d, porosity_2d), + darsia.weight(vtu_image_1d, porosity_1d), + ] + ) + + # Plot for testing purposes + if False: + vtu_image.show("equi-dimensionsional reconstruction") + + # Concentrations - simple since the equi-dimensional vtu images are interpreted as + # volumetric concentration + vtu_concentration = vtu_image.copy() + + return vtu_concentration + + +# ! --- Align DICOM and vtu images + + +def align_images(dicom_concentration, vtu_concentration): + # Plot two exemplary images to identify suitable src and dst points which will define + # the alignment procedure + + if True: + plt.figure("dicom") + plt.imshow(np.sum(dicom_concentration.img, axis=0)) + plt.figure("vtu") + plt.imshow(np.sum(vtu_concentration.img, axis=0)) + plt.show() + + # Pixels of fracture end points + voxels_src = [[0, 85, 0], [0, 85, 86], [17, 85, 0], [17, 85, 86]] # in DICOM image + voxels_dst = [ + [0, 237, 86.5], + [0, 148, 86.5], + [17, 237, 86.5], + [17, 148, 86.5], + ] # in VTU image + + # Define coordinate transform and apply it + coordinatesystem_src = dicom_concentration.coordinatesystem + coordinatesystem_dst = vtu_concentration.coordinatesystem + coordinate_transformation = darsia.CoordinateTransformation( + coordinatesystem_src, + coordinatesystem_dst, + voxels_src, + voxels_dst, + fit_options={ + "tol": 1e-5, + "preconditioning": True, + }, + isometry=False, + ) + transformed_dicom_concentration = coordinate_transformation(dicom_concentration) + + # Restrict to intersecting active canvas + intersection = coordinate_transformation.find_intersection() + aligned_dicom_concentration = transformed_dicom_concentration.subregion( + voxels=intersection + ) + aligned_vtu_concentration = vtu_concentration.subregion(voxels=intersection) + + return aligned_dicom_concentration, aligned_vtu_concentration + + +def qualitative_comparison( + mode: str, + full_dicom_image: darsia.Image, + full_vtu_image: darsia.Image, + image_path: Path, +): + ############################################################################## + # Plot the reconstructed vtu data, vtu plus Gaussian noise, and the dicom data. + import matplotlib + import matplotlib.cm as cm + + if mode == "sum": + dicom_image = darsia.reduce_axis(full_dicom_image, axis="z", mode="average") + vtu_image = darsia.reduce_axis(full_vtu_image, axis="z", mode="average") + else: + dicom_image = darsia.reduce_axis( + full_dicom_image, axis="z", mode="slice", depth=20 + ) + vtu_image = darsia.reduce_axis(full_vtu_image, axis="z", mode="slice", depth=20) + + # Define some plotting options (font, x and ylabels) + # matplotlib.rcParams.update({"font.size": 14}) + # Define x and y labels in cm (have to convert from m to cm) + shape = dicom_image.num_voxels + dimensions = dicom_image.dimensions + + x_pixel, y_pixel = np.meshgrid( + np.linspace(dimensions[0], 0, shape[0]), + np.linspace(0, dimensions[1], shape[1]), + indexing="ij", + ) + vmax = 1.25 + vmin = 0 + contourlevels = [0.045 * vmax, 0.055 * vmax] + cmap = "turbo" + + # Plot the reconstructed vtu data. Add a contour line which + # is only to aid the qualitative comparison of DICOM and vtu + # results with focus on the front. + fig_vtu, axs_vtu = plt.subplots(nrows=1, ncols=1) + axs_vtu.pcolormesh(y_pixel, x_pixel, vtu_image.img, cmap=cmap, vmin=vmin, vmax=vmax) + axs_vtu.contourf( + y_pixel, x_pixel, vtu_image.img, cmap="Reds", levels=contourlevels, alpha=0.5 + ) + axs_vtu.set_ylim(top=0.08) + axs_vtu.set_aspect("equal") + axs_vtu.set_xlabel("x [cm]") # , fontsize=14) + axs_vtu.set_ylabel("y [cm]") # , fontsize=14) + fig_vtu.colorbar(cm.ScalarMappable(cmap=cmap), ax=axs_vtu) + + # Plot the dicom data with contour line. + fig_dicom, axs_dicom = plt.subplots(nrows=1, ncols=1) + axs_dicom.pcolormesh( + y_pixel, x_pixel, dicom_image.img, cmap=cmap, vmin=vmin, vmax=vmax + ) + axs_dicom.contourf( + y_pixel, x_pixel, vtu_image.img, cmap="Reds", levels=contourlevels, alpha=0.5 + ) + axs_dicom.set_ylim(top=0.08) + axs_dicom.set_aspect("equal") + axs_dicom.set_xlabel("x [cm]") # , fontsize=14) + axs_dicom.set_ylabel("y [cm]") # , fontsize=14) + fig_dicom.colorbar(cm.ScalarMappable(cmap=cmap), ax=axs_dicom) + + # Plot the dicom data with contour line. + fig_combination, axs_combination = plt.subplots(nrows=1, ncols=1) + combined_image = dicom_image.img.copy() + mid = 86 + combined_image[:, mid:] = vtu_image.img[:, mid:] + axs_combination.pcolormesh( + y_pixel, x_pixel, combined_image, cmap=cmap, vmin=vmin, vmax=vmax + ) + axs_combination.contourf( + y_pixel, x_pixel, vtu_image.img, cmap="Reds", levels=contourlevels, alpha=0.5 + ) + axs_combination.plot( + [3.45 * cm2m, 3.45 * cm2m], + [0.0, 0.08], + color="white", + alpha=0.3, + linestyle="dashed", + ) + axs_combination.text( + 0.005, 0.075, "experiment", color="white", alpha=0.5, rotation=0, fontsize=14 + ) + axs_combination.text( + 0.04, 0.075, "simulation", color="white", alpha=0.5, rotation=0, fontsize=14 + ) + axs_combination.set_ylim(top=0.08) + axs_combination.set_aspect("equal") + axs_combination.set_xlabel("x [cm]", fontsize=14) + axs_combination.set_ylabel("y [cm]", fontsize=14) + fig_combination.colorbar( + cm.ScalarMappable(cmap=cmap), + ax=axs_combination, + label="volumetric concentration", + ) + fig_combination.savefig(image_path, dpi=500, transparent=True) + + plt.show() + + +########################################################################### +# Main analysis + +calibration_interval = slice(1, 8) + +# Original PET images +if False: + dicom_image_3d = read_dicom_images() + dicom_image_3d.save("dicom_raw_3d.npz") +else: + dicom_image_3d = darsia.imread("dicom_raw_3d.npz") + +# Pick corresponding vtu images. +vtu_2d_concentration = read_vtu_images() + +# Resize to similar shape as dicom image +dicom_voxel_size = dicom_image_3d.voxel_size +vtu_2d_concentration = darsia.equalize_voxel_size( + vtu_2d_concentration, min(dicom_voxel_size) +) +# vtu_2d_concentration.show() + +# Expand vtu image to 3d +dicom_height = dicom_image_3d.dimensions[0] +dicom_shape = dicom_image_3d.img.shape +vtu_concentration_3d = darsia.extrude_along_axis( + vtu_2d_concentration, dicom_height, dicom_shape[0] +) + +# Align dicom and vtu +if False: + dicom_concentration = dicom_image_3d.copy() + aligned_dicom_concentration, aligned_vtu_concentration = align_images( + dicom_concentration, vtu_concentration_3d + ) + aligned_dicom_concentration.save("aligned_dicom_concentration.npz") + aligned_vtu_concentration.save("aligned_vtu_concentration.npz") +else: + aligned_dicom_concentration = darsia.imread("aligned_dicom_concentration.npz") + aligned_vtu_concentration = darsia.imread("aligned_vtu_concentration.npz") + +# Define final vtu concentration, and compute its mass (reference) +vtu_concentration_3d = aligned_vtu_concentration.copy() +vtu_3d_shape = vtu_concentration_3d.shape_metadata() +vtu_3d_geometry = darsia.Geometry(**vtu_3d_shape) +vtu_3d_integral = vtu_3d_geometry.integrate(vtu_concentration_3d) + + +# DICOM without TVD +def rescale_data(image, ref_integral): + shape = image.shape_metadata() + geometry = darsia.Geometry(**shape) + integral = geometry.integrate(image) + image.img *= ref_integral / integral + return image + + +# Define dicom concentration with same mass +dicom_concentration_3d = aligned_dicom_concentration.copy() +dicom_concentration_3d = rescale_data(dicom_concentration_3d, vtu_3d_integral) + +# Define mask (omega) for trust for regularization +heterogeneous_omega = False +if heterogeneous_omega: + dicom_rescaled = dicom_concentration_3d.copy() + dicom_rescaled.img /= np.max(dicom_rescaled.img) + omega_bound = 0.15 + omega = np.minimum(dicom_rescaled.img, omega_bound) + mask_zero = dicom_rescaled.img < 1e-4 + omega[mask_zero] = 1 + plot_slice(omega) +else: + omega = 0.015 + +# DICOM concentration with H1 regularization +if True: + h1_reg_dicom_concentration_3d = darsia.H1_regularization( + dicom_concentration_3d, + mu=0.1, + omega=omega, + dim=3, + solver=darsia.CG(maxiter=10000, tol=1e-5), + ) + h1_reg_dicom_concentration_3d.save("h1_reg_dicom_concentration.npz") +else: + h1_reg_dicom_concentration = darsia.imread("h1_reg_dicom_concentration.npz") +h1_reg_dicom_concentration_3d = rescale_data( + h1_reg_dicom_concentration_3d, vtu_3d_integral +) + +# DICOM concentration with TVD regularization +if True: + tvd_reg_dicom_concentration_3d = darsia.tvd( + dicom_concentration_3d, + method="heterogeneous bregman", + isotropic=True, + weight=0.005, + omega=omega, + dim=3, + max_num_iter=100, + eps=1e-5, + verbose=True, + solver=darsia.Jacobi(maxiter=20), + ) + tvd_reg_dicom_concentration_3d.save("tvd_reg_dicom_concentration.npz") +else: + tvd_reg_dicom_concentration = darsia.imread("tvd_reg_dicom_concentration.npz") +tvd_reg_dicom_concentration_3d = rescale_data( + tvd_reg_dicom_concentration_3d, vtu_3d_integral +) + +# Make qualitative comparisons +plot_sum( + [ + vtu_concentration_3d, + dicom_concentration_3d, + h1_reg_dicom_concentration_3d, + tvd_reg_dicom_concentration_3d, + ] +) +plot_slice( + [ + vtu_concentration_3d, + dicom_concentration_3d, + h1_reg_dicom_concentration_3d, + tvd_reg_dicom_concentration_3d, + ] +) +qualitative_comparison( + "sum", dicom_concentration_3d, vtu_concentration_3d, "pure_dicom_avg.png" +) +qualitative_comparison( + "slice", dicom_concentration_3d, vtu_concentration_3d, "pure_dicom_slice.png" +) +qualitative_comparison( + "sum", + h1_reg_dicom_concentration_3d, + vtu_concentration_3d, + "h1_reg_dicom_avg_heter.png" if heterogeneous_omega else "h1_reg_dicom_avg_hom.png", +) +qualitative_comparison( + "slice", + h1_reg_dicom_concentration_3d, + vtu_concentration_3d, + "h1_reg_dicom_slice_heter.png" + if heterogeneous_omega + else "h1_reg_dicom_slice_hom.png", +) +qualitative_comparison( + "sum", + tvd_reg_dicom_concentration_3d, + vtu_concentration_3d, + "tvd_reg_dicom_avg_heter.png" + if heterogeneous_omega + else "tvd_reg_dicom_avg_hom.png", +) +qualitative_comparison( + "slice", + tvd_reg_dicom_concentration_3d, + vtu_concentration_3d, + "tvd_reg_dicom_slice_heter.png" + if heterogeneous_omega + else "tvd_reg_dicom_slice_hom.png", +) From 17fb8d8f33c04cba69904c3dcb9ef62e61cc71ed Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Thu, 6 Jul 2023 01:38:27 +0200 Subject: [PATCH 002/100] EX: USe suffix, add plot. --- examples/paper/pet_simulations_comparison.py | 61 ++++++++++++++------ 1 file changed, 43 insertions(+), 18 deletions(-) diff --git a/examples/paper/pet_simulations_comparison.py b/examples/paper/pet_simulations_comparison.py index 74686c19..c5a25a6e 100644 --- a/examples/paper/pet_simulations_comparison.py +++ b/examples/paper/pet_simulations_comparison.py @@ -2,9 +2,9 @@ """ -import time from pathlib import Path +import matplotlib.cm as cm import matplotlib.pyplot as plt import numpy as np @@ -456,8 +456,6 @@ def qualitative_comparison( ): ############################################################################## # Plot the reconstructed vtu data, vtu plus Gaussian noise, and the dicom data. - import matplotlib - import matplotlib.cm as cm if mode == "sum": dicom_image = darsia.reduce_axis(full_dicom_image, axis="z", mode="average") @@ -621,9 +619,34 @@ def rescale_data(image, ref_integral): mask_zero = dicom_rescaled.img < 1e-4 omega[mask_zero] = 1 plot_slice(omega) + + # Save plot + reduced_dicom_concentration_2d = darsia.reduce_axis( + dicom_concentration_3d, axis="z", mode="slice", depth=20 + ) + shape = reduced_dicom_concentration_2d.num_voxels + dimensions = reduced_dicom_concentration_2d.dimensions + + x_pixel, y_pixel = np.meshgrid( + np.linspace(dimensions[0], 0, shape[0]), + np.linspace(0, dimensions[1], shape[1]), + indexing="ij", + ) + fig_mask, axs_mask = plt.subplots(nrows=1, ncols=1) + axs_mask.pcolormesh(y_pixel, x_pixel, omega[20], cmap="turbo") + axs_mask.set_ylim(top=0.08) + axs_mask.set_aspect("equal") + axs_mask.set_xlabel("x [cm]") # , fontsize=14) + axs_mask.set_ylabel("y [cm]") # , fontsize=14) + fig_mask.colorbar(cm.ScalarMappable(cmap="turbo"), ax=axs_mask) + fig_mask.savefig("omega.png", dpi=500, transparent=True) + plt.close() else: omega = 0.015 +# Choose file suffix depeneding on omega +suffix = "heter" if heterogeneous_omega else "hom" + # DICOM concentration with H1 regularization if True: h1_reg_dicom_concentration_3d = darsia.H1_regularization( @@ -633,19 +656,23 @@ def rescale_data(image, ref_integral): dim=3, solver=darsia.CG(maxiter=10000, tol=1e-5), ) - h1_reg_dicom_concentration_3d.save("h1_reg_dicom_concentration.npz") + h1_reg_dicom_concentration_3d.save(f"h1_reg_dicom_concentration_{suffix}.npz") else: - h1_reg_dicom_concentration = darsia.imread("h1_reg_dicom_concentration.npz") + h1_reg_dicom_concentration = darsia.imread( + f"h1_reg_dicom_concentration_{suffix}.npz" + ) h1_reg_dicom_concentration_3d = rescale_data( h1_reg_dicom_concentration_3d, vtu_3d_integral ) # DICOM concentration with TVD regularization +isotropic = True +isotropic_suffix = "iso" if isotropic else "aniso" if True: tvd_reg_dicom_concentration_3d = darsia.tvd( dicom_concentration_3d, method="heterogeneous bregman", - isotropic=True, + isotropic=isotropic, weight=0.005, omega=omega, dim=3, @@ -654,9 +681,13 @@ def rescale_data(image, ref_integral): verbose=True, solver=darsia.Jacobi(maxiter=20), ) - tvd_reg_dicom_concentration_3d.save("tvd_reg_dicom_concentration.npz") + tvd_reg_dicom_concentration_3d.save( + f"tvd_{isotropic_suffix}_reg_dicom_concentration_{suffix}.npz" + ) else: - tvd_reg_dicom_concentration = darsia.imread("tvd_reg_dicom_concentration.npz") + tvd_reg_dicom_concentration = darsia.imread( + f"tvd_{isotropic_suffix}_reg_dicom_concentration_{suffix}.npz" + ) tvd_reg_dicom_concentration_3d = rescale_data( tvd_reg_dicom_concentration_3d, vtu_3d_integral ) @@ -688,29 +719,23 @@ def rescale_data(image, ref_integral): "sum", h1_reg_dicom_concentration_3d, vtu_concentration_3d, - "h1_reg_dicom_avg_heter.png" if heterogeneous_omega else "h1_reg_dicom_avg_hom.png", + f"h1_reg_dicom_avg_{suffix}.png", ) qualitative_comparison( "slice", h1_reg_dicom_concentration_3d, vtu_concentration_3d, - "h1_reg_dicom_slice_heter.png" - if heterogeneous_omega - else "h1_reg_dicom_slice_hom.png", + f"h1_reg_dicom_slice_{suffix}.png", ) qualitative_comparison( "sum", tvd_reg_dicom_concentration_3d, vtu_concentration_3d, - "tvd_reg_dicom_avg_heter.png" - if heterogeneous_omega - else "tvd_reg_dicom_avg_hom.png", + f"tvd_{isotropic_suffix}_reg_dicom_avg_{suffix}.png", ) qualitative_comparison( "slice", tvd_reg_dicom_concentration_3d, vtu_concentration_3d, - "tvd_reg_dicom_slice_heter.png" - if heterogeneous_omega - else "tvd_reg_dicom_slice_hom.png", + f"tvd_{isotropic_suffix}_reg_dicom_slice_{suffix}.png", ) From 44e7b5cc335db5bd22ea7f3d218956959b40cdf9 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Thu, 6 Jul 2023 07:14:04 +0200 Subject: [PATCH 003/100] ENH: More control on plotting of W1-dist results. --- src/darsia/measure/wasserstein.py | 60 +++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index 655db865..a12aa307 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -490,40 +490,71 @@ def _plot_solution( indexing="ij", ) + # Control of flux arrows scaling = self.options.get("scaling", 1.0) + frequency = self.options.get("plot_frequency", 1) # Plot the fluxes and pressure - plt.figure("Beckman solution") + plt.figure("Beckman pressure solution") plt.pcolormesh(X, Y, pressure, cmap="turbo") - plt.colorbar() + plt.colorbar(label="pressure") plt.quiver( - X, - Y, - scaling * flux[:, :, 0], - -scaling * flux[:, :, 1], + X[::frequency, ::frequency], + Y[::frequency, ::frequency], + scaling * flux[::frequency, ::frequency, 0], + -scaling * flux[::frequency, ::frequency, 1], angles="xy", scale_units="xy", scale=1, - alpha=0.5, + alpha=0.25, + width=0.005, ) + plt.xlabel("x [m]") + plt.ylabel("y [m]") + plt.ylim(top=0.08) + if self.options["name"] is not None: + plt.savefig( + self.options["name"] + "_beckman_pressure_solution.png", + dpi=500, + transparent=True, + ) plt.figure("Beckman solution fluxes") plt.pcolormesh(X, Y, mass_diff, cmap="turbo") - plt.colorbar() + plt.colorbar(label="mass difference") plt.quiver( - X, - Y, - scaling * flux[:, :, 0], - -scaling * flux[:, :, 1], + X[::frequency, ::frequency], + Y[::frequency, ::frequency], + scaling * flux[::frequency, ::frequency, 0], + -scaling * flux[::frequency, ::frequency, 1], angles="xy", scale_units="xy", scale=1, - alpha=0.5, + alpha=0.25, + width=0.005, ) + plt.xlabel("x [m]") + plt.ylabel("y [m]") + plt.ylim(top=0.08) + if self.options["name"] is not None: + plt.savefig( + self.options["name"] + "_beckman_solution_fluxes.png", + dpi=500, + transparent=True, + ) plt.figure("Beckman solution mobility") plt.pcolormesh(X, Y, mobility, cmap="turbo") - plt.colorbar() + plt.colorbar(label="flux modulus") + plt.xlabel("x [m]") + plt.ylabel("y [m]") + plt.ylim(top=0.08) + if self.options["name"] is not None: + plt.savefig( + self.options["name"] + "_beckman_solution_mobility.png", + dpi=500, + transparent=True, + ) plt.show() @@ -861,6 +892,7 @@ def wasserstein_distance( plot_solution = kwargs.get("plot_solution", False) return_solution = kwargs.get("return_solution", False) options = kwargs.get("options", {}) + options["name"] = kwargs.get("name") if method.lower() == "newton": w1 = WassersteinDistanceNewton(shape, voxel_size, dim, options) From d70cb2defb21dea71a8e7cee0e08fc2496a5878c Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Thu, 6 Jul 2023 09:17:57 +0200 Subject: [PATCH 004/100] EX: Add more advanced plots, and add 3d wasserstein. --- examples/paper/pet_simulations_comparison.py | 260 +++++++++++++------ 1 file changed, 176 insertions(+), 84 deletions(-) diff --git a/examples/paper/pet_simulations_comparison.py b/examples/paper/pet_simulations_comparison.py index c5a25a6e..ff4e63d9 100644 --- a/examples/paper/pet_simulations_comparison.py +++ b/examples/paper/pet_simulations_comparison.py @@ -333,24 +333,19 @@ def plot_concentration_evolution(concentration_2d): # ! ---- VTU images -def read_vtu_images() -> darsia.Image: +def read_vtu_images(vtu_ind: int) -> darsia.Image: """Read-in all available VTU data.""" # This corresponds approx. to (only temporary - not used further): vtu_time = [420 + 240 * i for i in range(8, 9)] # The corresponding indices used for storing the simulation data: - vtu_indices = [str(i).zfill(6) for i in [84 + 48 * j for j in range(7, 8)]] # vtu_indices = [str(i).zfill(6) for i in [84 + 48 * j for j in range(8, 9)]] - vtu_root = Path(".") + # vtu_ind = 416 # Find the corresponding files - vtu_images_2d = [ - vtu_root / Path(f"data_2_{str(ind).zfill(6)}.vtu") for ind in vtu_indices - ] - vtu_images_1d = [ - vtu_root / Path(f"data_1_{str(ind).zfill(6)}.vtu") for ind in vtu_indices - ] + vtu_images_2d = Path(f"data_2_{str(vtu_ind).zfill(6)}.vtu") + vtu_images_1d = Path(f"data_1_{str(vtu_ind).zfill(6)}.vtu") # Define vtu images as DarSIA images vtu_image_2d = darsia.imread( @@ -370,10 +365,6 @@ def read_vtu_images() -> darsia.Image: width=fracture_aperture, # for embedding in 2d ) - # Make vtu images series = False - vtu_image_2d = vtu_image_2d.time_slice(0) - vtu_image_1d = vtu_image_1d.time_slice(0) - # PorePy/meshio cuts off coordinate values at some point... # Correct manually - width / 2 - aperature / 2. vtu_image_1d.origin[0] = (6.98 / 2 - 0.1 / 2) * cm2m @@ -452,6 +443,7 @@ def qualitative_comparison( mode: str, full_dicom_image: darsia.Image, full_vtu_image: darsia.Image, + add_on: str, image_path: Path, ): ############################################################################## @@ -462,9 +454,11 @@ def qualitative_comparison( vtu_image = darsia.reduce_axis(full_vtu_image, axis="z", mode="average") else: dicom_image = darsia.reduce_axis( - full_dicom_image, axis="z", mode="slice", depth=20 + full_dicom_image, axis="z", mode="slice", slice_idx=20 + ) + vtu_image = darsia.reduce_axis( + full_vtu_image, axis="z", mode="slice", slice_idx=20 ) - vtu_image = darsia.reduce_axis(full_vtu_image, axis="z", mode="slice", depth=20) # Define some plotting options (font, x and ylabels) # matplotlib.rcParams.update({"font.size": 14}) @@ -482,34 +476,6 @@ def qualitative_comparison( contourlevels = [0.045 * vmax, 0.055 * vmax] cmap = "turbo" - # Plot the reconstructed vtu data. Add a contour line which - # is only to aid the qualitative comparison of DICOM and vtu - # results with focus on the front. - fig_vtu, axs_vtu = plt.subplots(nrows=1, ncols=1) - axs_vtu.pcolormesh(y_pixel, x_pixel, vtu_image.img, cmap=cmap, vmin=vmin, vmax=vmax) - axs_vtu.contourf( - y_pixel, x_pixel, vtu_image.img, cmap="Reds", levels=contourlevels, alpha=0.5 - ) - axs_vtu.set_ylim(top=0.08) - axs_vtu.set_aspect("equal") - axs_vtu.set_xlabel("x [cm]") # , fontsize=14) - axs_vtu.set_ylabel("y [cm]") # , fontsize=14) - fig_vtu.colorbar(cm.ScalarMappable(cmap=cmap), ax=axs_vtu) - - # Plot the dicom data with contour line. - fig_dicom, axs_dicom = plt.subplots(nrows=1, ncols=1) - axs_dicom.pcolormesh( - y_pixel, x_pixel, dicom_image.img, cmap=cmap, vmin=vmin, vmax=vmax - ) - axs_dicom.contourf( - y_pixel, x_pixel, vtu_image.img, cmap="Reds", levels=contourlevels, alpha=0.5 - ) - axs_dicom.set_ylim(top=0.08) - axs_dicom.set_aspect("equal") - axs_dicom.set_xlabel("x [cm]") # , fontsize=14) - axs_dicom.set_ylabel("y [cm]") # , fontsize=14) - fig_dicom.colorbar(cm.ScalarMappable(cmap=cmap), ax=axs_dicom) - # Plot the dicom data with contour line. fig_combination, axs_combination = plt.subplots(nrows=1, ncols=1) combined_image = dicom_image.img.copy() @@ -531,6 +497,9 @@ def qualitative_comparison( axs_combination.text( 0.005, 0.075, "experiment", color="white", alpha=0.5, rotation=0, fontsize=14 ) + axs_combination.text( + 0.005, 0.072, add_on, color="white", alpha=0.5, rotation=0, fontsize=14 + ) axs_combination.text( 0.04, 0.075, "simulation", color="white", alpha=0.5, rotation=0, fontsize=14 ) @@ -551,8 +520,6 @@ def qualitative_comparison( ########################################################################### # Main analysis -calibration_interval = slice(1, 8) - # Original PET images if False: dicom_image_3d = read_dicom_images() @@ -561,7 +528,8 @@ def qualitative_comparison( dicom_image_3d = darsia.imread("dicom_raw_3d.npz") # Pick corresponding vtu images. -vtu_2d_concentration = read_vtu_images() +vtu_ind = 416 +vtu_2d_concentration = read_vtu_images(vtu_ind) # Resize to similar shape as dicom image dicom_voxel_size = dicom_image_3d.voxel_size @@ -584,10 +552,12 @@ def qualitative_comparison( dicom_concentration, vtu_concentration_3d ) aligned_dicom_concentration.save("aligned_dicom_concentration.npz") - aligned_vtu_concentration.save("aligned_vtu_concentration.npz") + aligned_vtu_concentration.save(f"aligned_vtu_{vtu_ind}_concentration.npz") else: aligned_dicom_concentration = darsia.imread("aligned_dicom_concentration.npz") - aligned_vtu_concentration = darsia.imread("aligned_vtu_concentration.npz") + aligned_vtu_concentration = darsia.imread( + f"aligned_vtu_{vtu_ind}_concentration.npz" + ) # Define final vtu concentration, and compute its mass (reference) vtu_concentration_3d = aligned_vtu_concentration.copy() @@ -610,7 +580,7 @@ def rescale_data(image, ref_integral): dicom_concentration_3d = rescale_data(dicom_concentration_3d, vtu_3d_integral) # Define mask (omega) for trust for regularization -heterogeneous_omega = False +heterogeneous_omega = True if heterogeneous_omega: dicom_rescaled = dicom_concentration_3d.copy() dicom_rescaled.img /= np.max(dicom_rescaled.img) @@ -622,7 +592,7 @@ def rescale_data(image, ref_integral): # Save plot reduced_dicom_concentration_2d = darsia.reduce_axis( - dicom_concentration_3d, axis="z", mode="slice", depth=20 + dicom_concentration_3d, axis="z", mode="slice", slice_idx=20 ) shape = reduced_dicom_concentration_2d.num_voxels dimensions = reduced_dicom_concentration_2d.dimensions @@ -648,7 +618,7 @@ def rescale_data(image, ref_integral): suffix = "heter" if heterogeneous_omega else "hom" # DICOM concentration with H1 regularization -if True: +if False: h1_reg_dicom_concentration_3d = darsia.H1_regularization( dicom_concentration_3d, mu=0.1, @@ -658,7 +628,7 @@ def rescale_data(image, ref_integral): ) h1_reg_dicom_concentration_3d.save(f"h1_reg_dicom_concentration_{suffix}.npz") else: - h1_reg_dicom_concentration = darsia.imread( + h1_reg_dicom_concentration_3d = darsia.imread( f"h1_reg_dicom_concentration_{suffix}.npz" ) h1_reg_dicom_concentration_3d = rescale_data( @@ -668,7 +638,7 @@ def rescale_data(image, ref_integral): # DICOM concentration with TVD regularization isotropic = True isotropic_suffix = "iso" if isotropic else "aniso" -if True: +if False: tvd_reg_dicom_concentration_3d = darsia.tvd( dicom_concentration_3d, method="heterogeneous bregman", @@ -685,7 +655,7 @@ def rescale_data(image, ref_integral): f"tvd_{isotropic_suffix}_reg_dicom_concentration_{suffix}.npz" ) else: - tvd_reg_dicom_concentration = darsia.imread( + tvd_reg_dicom_concentration_3d = darsia.imread( f"tvd_{isotropic_suffix}_reg_dicom_concentration_{suffix}.npz" ) tvd_reg_dicom_concentration_3d = rescale_data( @@ -693,49 +663,171 @@ def rescale_data(image, ref_integral): ) # Make qualitative comparisons -plot_sum( - [ +if False: + plot_sum( + [ + vtu_concentration_3d, + dicom_concentration_3d, + h1_reg_dicom_concentration_3d, + tvd_reg_dicom_concentration_3d, + ] + ) + plot_slice( + [ + vtu_concentration_3d, + dicom_concentration_3d, + h1_reg_dicom_concentration_3d, + tvd_reg_dicom_concentration_3d, + ] + ) + qualitative_comparison( + "sum", + dicom_concentration_3d, vtu_concentration_3d, + "noisy", + "pure_dicom_avg.png", + ) + qualitative_comparison( + "slice", dicom_concentration_3d, + vtu_concentration_3d, + "noisy", + "pure_dicom_slice.png", + ) + qualitative_comparison( + "sum", h1_reg_dicom_concentration_3d, - tvd_reg_dicom_concentration_3d, - ] -) -plot_slice( - [ vtu_concentration_3d, - dicom_concentration_3d, + "H1", + f"h1_reg_dicom_avg_{suffix}.png", + ) + qualitative_comparison( + "slice", h1_reg_dicom_concentration_3d, + vtu_concentration_3d, + "H1", + f"h1_reg_dicom_slice_{suffix}.png", + ) + qualitative_comparison( + "sum", tvd_reg_dicom_concentration_3d, - ] + vtu_concentration_3d, + "TVD", + f"tvd_{isotropic_suffix}_reg_dicom_avg_{suffix}.png", + ) + qualitative_comparison( + "slice", + tvd_reg_dicom_concentration_3d, + vtu_concentration_3d, + "TVD", + f"tvd_{isotropic_suffix}_reg_dicom_slice_{suffix}.png", + ) + +# Quantitative comparison - Wasserstein distance in 2d (only for illustration purposes) + +slice_idx = 20 +dicom_slice = darsia.reduce_axis( + dicom_concentration_3d, axis="z", mode="slice", slice_idx=slice_idx ) -qualitative_comparison( - "sum", dicom_concentration_3d, vtu_concentration_3d, "pure_dicom_avg.png" +vtu_slice = darsia.reduce_axis( + vtu_concentration_3d, axis="z", mode="slice", slice_idx=slice_idx ) -qualitative_comparison( - "slice", dicom_concentration_3d, vtu_concentration_3d, "pure_dicom_slice.png" +h1_reg_dicom_slice = darsia.reduce_axis( + h1_reg_dicom_concentration_3d, axis="z", mode="slice", slice_idx=slice_idx ) -qualitative_comparison( - "sum", - h1_reg_dicom_concentration_3d, - vtu_concentration_3d, - f"h1_reg_dicom_avg_{suffix}.png", +tvd_reg_dicom_slice = darsia.reduce_axis( + tvd_reg_dicom_concentration_3d, axis="z", mode="slice", slice_idx=slice_idx ) -qualitative_comparison( - "slice", - h1_reg_dicom_concentration_3d, - vtu_concentration_3d, - f"h1_reg_dicom_slice_{suffix}.png", + + +# Rescale (unphysical but necessary for comparison) +def rescale_slice(image: darsia.Image, ref_integral) -> darsia.Image: + shape = image.shape_metadata() + geometry = darsia.Geometry(**shape) + integral = geometry.integrate(image) + image.img *= ref_integral / integral + return image + + +# Determine reference value +shape = vtu_slice.shape_metadata() +geometry = darsia.Geometry(**shape) +ref_integral = geometry.integrate(vtu_slice) + +dicom_slice = rescale_slice(dicom_slice, ref_integral) +h1_reg_dicom_slice = rescale_slice(h1_reg_dicom_slice, ref_integral) +tvd_reg_dicom_slice = rescale_slice(tvd_reg_dicom_slice, ref_integral) + +# Wasserstein for 2d images +options = { + "method": "newton", + "L": 1e-2, + "num_iter": 100, + "tol": 1e-10, + "tol_distance": 1e-11, + "regularization": 1e-16, + "depth": 10, + "lumping": True, + "verbose": True, + "scaling": 10, + "plot_frequency": 10, +} +kwargs = { + "method": "newton", + "options": options, + "plot_solution": True, +} +distance_dicom_vtu = darsia.wasserstein_distance( + dicom_slice, vtu_slice, name="pure dicom vs. vtu", **kwargs ) -qualitative_comparison( - "sum", - tvd_reg_dicom_concentration_3d, +distance_h1_dicom_vtu = darsia.wasserstein_distance( + h1_reg_dicom_slice, vtu_slice, name="h1 reg dicom vs. vtu", **kwargs +) +distance_tvd_dicom_vtu = darsia.wasserstein_distance( + tvd_reg_dicom_slice, vtu_slice, name="tvd reg dicom vs. vtu", **kwargs +) + +print("The distances:") +print("Pure DICOM: ", distance_dicom_vtu) +print("H1 reg DICOM: ", distance_h1_dicom_vtu) +print("TVD reg DICOM: ", distance_tvd_dicom_vtu) + +assert False, "3d Wasserstein distance computations not sufficiently efficient." + +# Quantitative comparison - Wasserstein distance in 3d +options = { + "method": "newton", + "L": 1e-2, + "num_iter": 100, + "tol": 1e-10, + "tol_distance": 1e-11, + "regularization": 1e-16, + "depth": 10, + "lumping": True, + "verbose": True, +} +kwargs = { + "method": "newton", + "options": options, + "plot_solution": False, +} +distance_dicom_vtu_3d = darsia.wasserstein_distance_3d( + dicom_concentration_3d, vtu_concentration_3d, name="pure dicom vs. vtu 3d", **kwargs +) +distance_h1_dicom_vtu_3d = darsia.wasserstein_distance_3d( + h1_reg_dicom_concentration_3d, vtu_concentration_3d, - f"tvd_{isotropic_suffix}_reg_dicom_avg_{suffix}.png", + name="h1 reg dicom vs. vtu 3d", + **kwargs, ) -qualitative_comparison( - "slice", +distance_tvd_dicom_vtu_3d = darsia.wasserstein_distance_3d( tvd_reg_dicom_concentration_3d, vtu_concentration_3d, - f"tvd_{isotropic_suffix}_reg_dicom_slice_{suffix}.png", + name="tvd reg dicom vs. vtu 3d", + **kwargs, ) + +print("The distances 3d:") +print("Pure DICOM: ", distance_dicom_vtu_3d) +print("H1 reg DICOM: ", distance_h1_dicom_vtu_3d) +print("TVD reg DICOM: ", distance_tvd_dicom_vtu_3d) From 6c989e4450cdbc2677867a4da58f2d6081560b56 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Thu, 6 Jul 2023 10:54:20 +0200 Subject: [PATCH 005/100] EX: Fix plots and rename script. --- ... => pet_simulations_comparison_block_b.py} | 225 +++++++++++------- 1 file changed, 140 insertions(+), 85 deletions(-) rename examples/paper/{pet_simulations_comparison.py => pet_simulations_comparison_block_b.py} (82%) diff --git a/examples/paper/pet_simulations_comparison.py b/examples/paper/pet_simulations_comparison_block_b.py similarity index 82% rename from examples/paper/pet_simulations_comparison.py rename to examples/paper/pet_simulations_comparison_block_b.py index ff4e63d9..8c4108bc 100644 --- a/examples/paper/pet_simulations_comparison.py +++ b/examples/paper/pet_simulations_comparison_block_b.py @@ -37,7 +37,6 @@ def read_dicom_images() -> darsia.Image: root / Path(p) / Path("DICOM/PA1/ST1/SE1") for p in [ "fractip b closed 1 min rekon start 0 15 frames", - # "fractip b closed 1 min frames start 3780", ] ] @@ -337,15 +336,11 @@ def read_vtu_images(vtu_ind: int) -> darsia.Image: """Read-in all available VTU data.""" # This corresponds approx. to (only temporary - not used further): - vtu_time = [420 + 240 * i for i in range(8, 9)] - - # The corresponding indices used for storing the simulation data: - # vtu_indices = [str(i).zfill(6) for i in [84 + 48 * j for j in range(8, 9)]] - # vtu_ind = 416 + vtu_time = 0 # not relevant here. # Find the corresponding files - vtu_images_2d = Path(f"data_2_{str(vtu_ind).zfill(6)}.vtu") - vtu_images_1d = Path(f"data_1_{str(vtu_ind).zfill(6)}.vtu") + vtu_images_2d = Path(f"data/vtu/block-b/data_2_{str(vtu_ind).zfill(6)}.vtu") + vtu_images_1d = Path(f"data/vtu/block-b/data_1_{str(vtu_ind).zfill(6)}.vtu") # Define vtu images as DarSIA images vtu_image_2d = darsia.imread( @@ -439,12 +434,69 @@ def align_images(dicom_concentration, vtu_concentration): return aligned_dicom_concentration, aligned_vtu_concentration +def single_plot( + mode: str, + full_image: darsia.Image, + add_on: str, + image_path: Path, +): + ############################################################################## + # Plot the reconstructed vtu data, vtu plus Gaussian noise, and the dicom data. + + if mode == "sum": + image = darsia.reduce_axis(full_image, axis="z", mode="average") + else: + image = darsia.reduce_axis( + full_image, axis="z", mode="slice", slice_idx=20 + ) + + # Define some plotting options (font, x and ylabels) + # matplotlib.rcParams.update({"font.size": 14}) + # Define x and y labels in cm (have to convert from m to cm) + shape = image.num_voxels + dimensions = image.dimensions + + x_pixel, y_pixel = np.meshgrid( + np.linspace(dimensions[0], 0, shape[0]), + np.linspace(0, dimensions[1], shape[1]), + indexing="ij", + ) + vmax = 1.25 + vmin = 0 + contourlevels = [0.045 * vmax, 0.055 * vmax] + cmap = "turbo" + + # Plot the data with contour line. + fig_combination, axs_combination = plt.subplots(nrows=1, ncols=1) + axs_combination.pcolormesh( + y_pixel, x_pixel, image.img, cmap=cmap, vmin=vmin, vmax=vmax + ) + axs_combination.text( + 0.005, 0.075, "experiment", color="white", alpha=0.5, rotation=0, fontsize=14 + ) + axs_combination.text( + 0.005, 0.070, add_on, color="white", alpha=0.5, rotation=0, fontsize=14 + ) + axs_combination.set_ylim(top=0.08) + axs_combination.set_aspect("equal") + axs_combination.set_xlabel("x [m]", fontsize=14) + axs_combination.set_ylabel("y [m]", fontsize=14) + fig_combination.colorbar( + cm.ScalarMappable(cmap=cmap), + ax=axs_combination, + label="volumetric concentration", + ) + fig_combination.savefig(image_path, dpi=500, transparent=True) + + plt.show() + def qualitative_comparison( mode: str, full_dicom_image: darsia.Image, full_vtu_image: darsia.Image, add_on: str, image_path: Path, + colorbar: bool= False ): ############################################################################## # Plot the reconstructed vtu data, vtu plus Gaussian noise, and the dicom data. @@ -498,20 +550,21 @@ def qualitative_comparison( 0.005, 0.075, "experiment", color="white", alpha=0.5, rotation=0, fontsize=14 ) axs_combination.text( - 0.005, 0.072, add_on, color="white", alpha=0.5, rotation=0, fontsize=14 + 0.005, 0.070, add_on, color="white", alpha=0.5, rotation=0, fontsize=14 ) axs_combination.text( 0.04, 0.075, "simulation", color="white", alpha=0.5, rotation=0, fontsize=14 ) axs_combination.set_ylim(top=0.08) axs_combination.set_aspect("equal") - axs_combination.set_xlabel("x [cm]", fontsize=14) - axs_combination.set_ylabel("y [cm]", fontsize=14) - fig_combination.colorbar( - cm.ScalarMappable(cmap=cmap), - ax=axs_combination, - label="volumetric concentration", - ) + axs_combination.set_xlabel("x [m]", fontsize=14) + axs_combination.set_ylabel("y [m]", fontsize=14) + if colorbar: + fig_combination.colorbar( + cm.ScalarMappable(cmap=cmap), + ax=axs_combination, + label="volumetric concentration", + ) fig_combination.savefig(image_path, dpi=500, transparent=True) plt.show() @@ -521,14 +574,14 @@ def qualitative_comparison( # Main analysis # Original PET images -if False: +if True: dicom_image_3d = read_dicom_images() - dicom_image_3d.save("dicom_raw_3d.npz") + dicom_image_3d.save("data/npz/block-b/dicom_raw_3d.npz") else: - dicom_image_3d = darsia.imread("dicom_raw_3d.npz") + dicom_image_3d = darsia.imread("data/npz/block-b/dicom_raw_3d.npz") # Pick corresponding vtu images. -vtu_ind = 416 +vtu_ind = 439 vtu_2d_concentration = read_vtu_images(vtu_ind) # Resize to similar shape as dicom image @@ -536,7 +589,6 @@ def qualitative_comparison( vtu_2d_concentration = darsia.equalize_voxel_size( vtu_2d_concentration, min(dicom_voxel_size) ) -# vtu_2d_concentration.show() # Expand vtu image to 3d dicom_height = dicom_image_3d.dimensions[0] @@ -546,17 +598,17 @@ def qualitative_comparison( ) # Align dicom and vtu -if False: +if True: dicom_concentration = dicom_image_3d.copy() aligned_dicom_concentration, aligned_vtu_concentration = align_images( dicom_concentration, vtu_concentration_3d ) - aligned_dicom_concentration.save("aligned_dicom_concentration.npz") - aligned_vtu_concentration.save(f"aligned_vtu_{vtu_ind}_concentration.npz") + aligned_dicom_concentration.save("data/npz/block-b/aligned_dicom_concentration.npz") + aligned_vtu_concentration.save(f"data/npz/block-b/aligned_vtu_{vtu_ind}_concentration.npz") else: - aligned_dicom_concentration = darsia.imread("aligned_dicom_concentration.npz") + aligned_dicom_concentration = darsia.imread("data/npz/block-b/aligned_dicom_concentration.npz") aligned_vtu_concentration = darsia.imread( - f"aligned_vtu_{vtu_ind}_concentration.npz" + f"data/npz/block-b/aligned_vtu_{vtu_ind}_concentration.npz" ) # Define final vtu concentration, and compute its mass (reference) @@ -578,6 +630,8 @@ def rescale_data(image, ref_integral): # Define dicom concentration with same mass dicom_concentration_3d = aligned_dicom_concentration.copy() dicom_concentration_3d = rescale_data(dicom_concentration_3d, vtu_3d_integral) +#single_plot("slice", dicom_concentration_3d, "noisy", "plots/block-b/lab_data_slice.png") +#single_plot("sum", dicom_concentration_3d, "noisy", "plots/block-b/lab_data_avg.png") # Define mask (omega) for trust for regularization heterogeneous_omega = True @@ -587,8 +641,8 @@ def rescale_data(image, ref_integral): omega_bound = 0.15 omega = np.minimum(dicom_rescaled.img, omega_bound) mask_zero = dicom_rescaled.img < 1e-4 - omega[mask_zero] = 1 - plot_slice(omega) + omega[mask_zero] = 10 + #plot_slice(omega) # Save plot reduced_dicom_concentration_2d = darsia.reduce_axis( @@ -603,13 +657,13 @@ def rescale_data(image, ref_integral): indexing="ij", ) fig_mask, axs_mask = plt.subplots(nrows=1, ncols=1) - axs_mask.pcolormesh(y_pixel, x_pixel, omega[20], cmap="turbo") + axs_mask.pcolormesh(y_pixel, x_pixel, np.log(omega[20]), cmap="turbo") axs_mask.set_ylim(top=0.08) axs_mask.set_aspect("equal") - axs_mask.set_xlabel("x [cm]") # , fontsize=14) - axs_mask.set_ylabel("y [cm]") # , fontsize=14) - fig_mask.colorbar(cm.ScalarMappable(cmap="turbo"), ax=axs_mask) - fig_mask.savefig("omega.png", dpi=500, transparent=True) + axs_mask.set_xlabel("x [m]") # , fontsize=14) + axs_mask.set_ylabel("y [m]") # , fontsize=14) + fig_mask.colorbar(cm.ScalarMappable(cmap="turbo"), ax=axs_mask, label="log($\omega_f$)") + fig_mask.savefig("plots/block-b/omega.png", dpi=500, transparent=True) plt.close() else: omega = 0.015 @@ -618,7 +672,7 @@ def rescale_data(image, ref_integral): suffix = "heter" if heterogeneous_omega else "hom" # DICOM concentration with H1 regularization -if False: +if True: h1_reg_dicom_concentration_3d = darsia.H1_regularization( dicom_concentration_3d, mu=0.1, @@ -626,10 +680,10 @@ def rescale_data(image, ref_integral): dim=3, solver=darsia.CG(maxiter=10000, tol=1e-5), ) - h1_reg_dicom_concentration_3d.save(f"h1_reg_dicom_concentration_{suffix}.npz") + h1_reg_dicom_concentration_3d.save(f"data/npz/block-b/h1_reg_dicom_concentration_{suffix}.npz") else: h1_reg_dicom_concentration_3d = darsia.imread( - f"h1_reg_dicom_concentration_{suffix}.npz" + f"data/npz/block-b/h1_reg_dicom_concentration_{suffix}.npz" ) h1_reg_dicom_concentration_3d = rescale_data( h1_reg_dicom_concentration_3d, vtu_3d_integral @@ -638,12 +692,12 @@ def rescale_data(image, ref_integral): # DICOM concentration with TVD regularization isotropic = True isotropic_suffix = "iso" if isotropic else "aniso" -if False: +if True: tvd_reg_dicom_concentration_3d = darsia.tvd( dicom_concentration_3d, method="heterogeneous bregman", isotropic=isotropic, - weight=0.005, + weight=0.02, omega=omega, dim=3, max_num_iter=100, @@ -652,75 +706,76 @@ def rescale_data(image, ref_integral): solver=darsia.Jacobi(maxiter=20), ) tvd_reg_dicom_concentration_3d.save( - f"tvd_{isotropic_suffix}_reg_dicom_concentration_{suffix}.npz" + f"data/npz/block-b/tvd_{isotropic_suffix}_reg_dicom_concentration_{suffix}.npz" ) else: tvd_reg_dicom_concentration_3d = darsia.imread( - f"tvd_{isotropic_suffix}_reg_dicom_concentration_{suffix}.npz" + f"data/npz/block-b/tvd_{isotropic_suffix}_reg_dicom_concentration_{suffix}.npz" ) tvd_reg_dicom_concentration_3d = rescale_data( tvd_reg_dicom_concentration_3d, vtu_3d_integral ) # Make qualitative comparisons -if False: - plot_sum( - [ - vtu_concentration_3d, - dicom_concentration_3d, - h1_reg_dicom_concentration_3d, - tvd_reg_dicom_concentration_3d, - ] - ) - plot_slice( - [ - vtu_concentration_3d, - dicom_concentration_3d, - h1_reg_dicom_concentration_3d, - tvd_reg_dicom_concentration_3d, - ] - ) - qualitative_comparison( - "sum", - dicom_concentration_3d, - vtu_concentration_3d, - "noisy", - "pure_dicom_avg.png", - ) +if True: + #plot_sum( + # [ + # vtu_concentration_3d, + # dicom_concentration_3d, + # h1_reg_dicom_concentration_3d, + # tvd_reg_dicom_concentration_3d, + # ] + #) + #plot_slice( + # [ + # vtu_concentration_3d, + # dicom_concentration_3d, + # h1_reg_dicom_concentration_3d, + # tvd_reg_dicom_concentration_3d, + # ] + #) + #qualitative_comparison( + # "sum", + # dicom_concentration_3d, + # vtu_concentration_3d, + # "noisy", + # "plots/block-b/pure_dicom_avg.png", + #) qualitative_comparison( "slice", dicom_concentration_3d, vtu_concentration_3d, "noisy", - "pure_dicom_slice.png", - ) - qualitative_comparison( - "sum", - h1_reg_dicom_concentration_3d, - vtu_concentration_3d, - "H1", - f"h1_reg_dicom_avg_{suffix}.png", - ) + "plots/block-b/pure_dicom_slice.png", + ) + #qualitative_comparison( + # "sum", + # h1_reg_dicom_concentration_3d, + # vtu_concentration_3d, + # "H1", + # f"plots/block-b/h1_reg_dicom_avg_{suffix}.png", + #) qualitative_comparison( "slice", h1_reg_dicom_concentration_3d, vtu_concentration_3d, "H1", - f"h1_reg_dicom_slice_{suffix}.png", + f"plots/block-b/h1_reg_dicom_slice_{suffix}.png", ) qualitative_comparison( "sum", tvd_reg_dicom_concentration_3d, vtu_concentration_3d, "TVD", - f"tvd_{isotropic_suffix}_reg_dicom_avg_{suffix}.png", + f"plots/block-b/tvd_{isotropic_suffix}_reg_dicom_avg_{suffix}.png", + colorbar= True ) qualitative_comparison( "slice", tvd_reg_dicom_concentration_3d, vtu_concentration_3d, "TVD", - f"tvd_{isotropic_suffix}_reg_dicom_slice_{suffix}.png", + f"plots/block-b/tvd_{isotropic_suffix}_reg_dicom_slice_{suffix}.png", ) # Quantitative comparison - Wasserstein distance in 2d (only for illustration purposes) @@ -762,9 +817,9 @@ def rescale_slice(image: darsia.Image, ref_integral) -> darsia.Image: options = { "method": "newton", "L": 1e-2, - "num_iter": 100, - "tol": 1e-10, - "tol_distance": 1e-11, + "num_iter": 500, + "tol": 1e-11, + "tol_distance": 1e-12, "regularization": 1e-16, "depth": 10, "lumping": True, @@ -778,13 +833,13 @@ def rescale_slice(image: darsia.Image, ref_integral) -> darsia.Image: "plot_solution": True, } distance_dicom_vtu = darsia.wasserstein_distance( - dicom_slice, vtu_slice, name="pure dicom vs. vtu", **kwargs + dicom_slice, vtu_slice, name="plots/block-b/pure dicom vs. vtu", **kwargs ) distance_h1_dicom_vtu = darsia.wasserstein_distance( - h1_reg_dicom_slice, vtu_slice, name="h1 reg dicom vs. vtu", **kwargs + h1_reg_dicom_slice, vtu_slice, name="plots/block-b/h1 reg dicom vs. vtu", **kwargs ) distance_tvd_dicom_vtu = darsia.wasserstein_distance( - tvd_reg_dicom_slice, vtu_slice, name="tvd reg dicom vs. vtu", **kwargs + tvd_reg_dicom_slice, vtu_slice, name="plots/block-b/tvd reg dicom vs. vtu", **kwargs ) print("The distances:") @@ -812,18 +867,18 @@ def rescale_slice(image: darsia.Image, ref_integral) -> darsia.Image: "plot_solution": False, } distance_dicom_vtu_3d = darsia.wasserstein_distance_3d( - dicom_concentration_3d, vtu_concentration_3d, name="pure dicom vs. vtu 3d", **kwargs + dicom_concentration_3d, vtu_concentration_3d, name="", **kwargs ) distance_h1_dicom_vtu_3d = darsia.wasserstein_distance_3d( h1_reg_dicom_concentration_3d, vtu_concentration_3d, - name="h1 reg dicom vs. vtu 3d", + name="", **kwargs, ) distance_tvd_dicom_vtu_3d = darsia.wasserstein_distance_3d( tvd_reg_dicom_concentration_3d, vtu_concentration_3d, - name="tvd reg dicom vs. vtu 3d", + name="", **kwargs, ) From f532876c72f6e74f340a799e33eeb43bc2e57b76 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Tue, 22 Aug 2023 15:08:03 +0200 Subject: [PATCH 006/100] ENH: init 3d version of wasserstein distance --- src/darsia/measure/wasserstein3d.py | 950 ++++++++++++++++++++++++++++ 1 file changed, 950 insertions(+) create mode 100644 src/darsia/measure/wasserstein3d.py diff --git a/src/darsia/measure/wasserstein3d.py b/src/darsia/measure/wasserstein3d.py new file mode 100644 index 00000000..e2b68778 --- /dev/null +++ b/src/darsia/measure/wasserstein3d.py @@ -0,0 +1,950 @@ +"""Wasserstein distance computed using variational methods. + +3d case: to migrate to general file after development. + +""" +from __future__ import annotations + +import time + +import matplotlib.pyplot as plt +import numpy as np +import scipy.sparse as sps + +import darsia + + +class VariationalWassersteinDistance3d(darsia.EMD): + """Base class for setting up the variational Wasserstein distance. + + The variational Wasserstein distance is defined as the solution to the following + optimization problem (also called the Beckman problem): + inf ||u||_{L^1} s.t. div u = m_1 - m_2, u in H(div). + u is the flux, m_1 and m_2 are the mass distributions which are transported by u + from m_1 to m_2. + + Specialized classes implement the solution of the Beckman problem using different + methods. There are two main methods: + - Newton's method (:class:`WassersteinDistanceNewton`) + - Split Bregman method (:class:`WassersteinDistanceBregman`) + + """ + + def __init__( + self, + shape: tuple, + voxel_size: list, + dim: int, + options: dict = {}, + ) -> None: + """ + Args: + + shape (tuple): shape of the image + voxel_size (list): voxel size of the image + dim (int): dimension of the problem + options (dict): options for the solver + - num_iter (int): maximum number of iterations. Defaults to 100. + - tol (float): tolerance for the stopping criterion. Defaults to 1e-6. + - L (float): parameter for the Bregman iteration. Defaults to 1.0. + - regularization (float): regularization parameter for the Bregman + iteration. Defaults to 0.0. + - depth (int): depth of the Anderson acceleration. Defaults to 0. + - scaling (float): scaling of the fluxes in the plot. Defaults to 1.0. + - lumping (bool): lump the mass matrix. Defaults to True. + + """ + # TODO improve documentation for options - method dependent + # Cache geometrical infos + self.shape = shape + self.voxel_size = voxel_size + self.dim = dim + + assert dim == 3, "Currently only 3D images are supported." + + self.options = options + self.regularization = self.options.get("regularization", 0.0) + self.verbose = self.options.get("verbose", False) + + # Setup + self._setup() + + def _setup(self) -> None: + """Setup of fixed discretization""" + + # Define dimensions of the problem + dim_cells = self.shape + num_cells = np.prod(dim_cells) + numbering_cells = np.arange(num_cells, dtype=int).reshape(dim_cells) + + # Consider only inner faces + vertical_faces_shape = (self.shape[0], self.shape[1] - 1, self.shape[2]) + horizontal_faces_shape = (self.shape[0] - 1, self.shape[1], self.shape[2]) + level_faces_shape = (self.shape[0], self.shape[1], self.shape[2] - 1) + num_faces_axis = [ + np.prod(vertical_faces_shape), + np.prod(horizontal_faces_shape), + np.prod(level_faces_shape), + ] + num_faces = np.sum(num_faces_axis) + + # Define connectivity + connectivity = np.zeros((num_faces, 2), dtype=int) + connectivity[: num_faces_axis[0], 0] = np.ravel( + numbering_cells[:, :-1, :] + ) # left cells + connectivity[: num_faces_axis[0], 1] = np.ravel( + numbering_cells[:, 1:, :] + ) # right cells + connectivity[ + num_faces_axis[0] : num_faces_axis[0] + num_faces_axis[1], 0 + ] = np.ravel( + numbering_cells[:-1, :, :] + ) # top cells + connectivity[ + num_faces_axis[0] : num_faces_axis[0] + num_faces_axis[1], 1 + ] = np.ravel( + numbering_cells[1:, :, :] + ) # bottom cells + connectivity[num_faces_axis[0] + num_faces_axis[1] :, 0] = np.ravel( + numbering_cells[:, :, :-1] + ) # FIXME (name?) cells + connectivity[num_faces_axis[0] + num_faces_axis[1] :, 1] = np.ravel( + numbering_cells[:, :, 1:] + ) # FIXME (name?) cells + + # Define sparse divergence operator, integrated over elements: flat_fluxes -> flat_mass + div_data = np.concatenate( + ( + self.voxel_size[0] * np.ones(num_faces_axis[0], dtype=float), + self.voxel_size[1] * np.ones(num_faces_axis[1], dtype=float), + self.voxel_size[2] * np.ones(num_faces_axis[2], dtype=float), + -self.voxel_size[0] * np.ones(num_faces_axis[0], dtype=float), + -self.voxel_size[1] * np.ones(num_faces_axis[1], dtype=float), + -self.voxel_size[2] * np.ones(num_faces_axis[2], dtype=float), + ) + ) + div_row = np.concatenate( + ( + connectivity[: num_faces_axis[0], 0], + connectivity[ + num_faces_axis[0] : num_faces_axis[0] + num_faces_axis[1], 0 + ], + connectivity[num_faces_axis[0] + num_faces_axis[1] :, 0], + connectivity[: num_faces_axis[0], 1], + connectivity[ + num_faces_axis[0] : num_faces_axis[0] + num_faces_axis[1], 1 + ], + connectivity[num_faces_axis[0] + num_faces_axis[1] :, 1], + ) + ) + div_col = np.tile(np.arange(num_faces, dtype=int), 2) + self.div = sps.csc_matrix( + (div_data, (div_row, div_col)), shape=(num_cells, num_faces) + ) + + # Define sparse mass matrix on cells: flat_mass -> flat_mass + self.mass_matrix_cells = sps.diags( + np.prod(self.voxel_size) * np.ones(num_cells, dtype=float) + ) + + # Define sparse mass matrix on faces: flat_fluxes -> flat_fluxes + lumping = self.options.get("lumping", True) + if lumping: + self.mass_matrix_faces = sps.diags( + np.prod(self.voxel_size) * np.ones(num_faces, dtype=float) + ) + else: + # Define connectivity: cell to face (only for inner cells) + connectivity_cell_to_vertical_face = np.zeros((num_cells, 2), dtype=int) + connectivity_cell_to_vertical_face[ + np.ravel(numbering_cells[:, :-1, :]), 0 + ] = np.arange( + num_faces_axis[0] + ) # left face + connectivity_cell_to_vertical_face[ + np.ravel(numbering_cells[:, 1:, :]), 1 + ] = np.arange( + num_faces_axis[0] + ) # right face + connectivity_cell_to_horizontal_face = np.zeros((num_cells, 2), dtype=int) + connectivity_cell_to_horizontal_face[ + np.ravel(numbering_cells[:-1, :, :]), 0 + ] = np.arange( + num_faces_axis[0], num_faces_axis[0] + num_faces_axis[1] + ) # top face + connectivity_cell_to_horizontal_face[ + np.ravel(numbering_cells[1:, :, :]), 1 + ] = np.arange( + num_faces_axis[0], num_faces_axis[0] + num_faces_axis[1] + ) # bottom face + connectivity_cell_to_level_face = np.zeros((num_cells, 2), dtype=int) + connectivity_cell_to_level_face[ + np.ravel(numbering_cells[:, :, :-1]), 0 + ] = np.arange( + num_faces_axis[0] + num_faces_axis[1], + num_faces_axis[0] + num_faces_axis[1] + num_faces_axis[2], + ) # FIXME (name?) face + connectivity_cell_to_level_face[ + np.ravel(numbering_cells[:, :, 1:]), 1 + ] = np.arange( + num_faces_axis[0] + num_faces_axis[1], + num_faces_axis[0] + num_faces_axis[1] + num_faces_axis[2], + ) # FIXME (name?) face + + # Info about inner cells + inner_cells_with_vertical_faces = np.ravel(numbering_cells[:, 1:-1, :]) + inner_cells_with_horizontal_faces = np.ravel(numbering_cells[1:-1, :, :]) + inner_cells_with_level_faces = np.ravel(numbering_cells[:, :, 1:-1]) + num_inner_cells_with_vertical_faces = len(inner_cells_with_vertical_faces) + num_inner_cells_with_horizontal_faces = len( + inner_cells_with_horizontal_faces + ) + num_inner_cells_with_level_faces = len(inner_cells_with_level_faces) + + # Define true RT0 mass matrix on faces: flat_fluxes -> flat_fluxes + mass_matrix_faces_data = np.prod(self.voxel_size) * np.concatenate( + ( + 2 / 3 * np.ones(num_faces, dtype=float), # all faces + 1 + / 6 + * np.ones( + num_inner_cells_with_vertical_faces, dtype=float + ), # left faces + 1 + / 6 + * np.ones( + num_inner_cells_with_vertical_faces, dtype=float + ), # right faces + 1 + / 6 + * np.ones( + num_inner_cells_with_horizontal_faces, dtype=float + ), # top faces + 1 + / 6 + * np.ones( + num_inner_cells_with_horizontal_faces, dtype=float + ), # bottom faces + 1 + / 6 + * np.ones( + num_inner_cells_with_level_faces, dtype=float + ), # FIXME (name?) faces + 1 + / 6 + * np.ones( + num_inner_cells_with_level_faces, dtype=float + ), # FIXME (name?) faces + ) + ) + mass_matrix_faces_row = np.concatenate( + ( + np.arange(num_faces, dtype=int), + connectivity_cell_to_vertical_face[ + inner_cells_with_vertical_faces, 0 + ], + connectivity_cell_to_vertical_face[ + inner_cells_with_vertical_faces, 1 + ], + connectivity_cell_to_horizontal_face[ + inner_cells_with_horizontal_faces, 0 + ], + connectivity_cell_to_horizontal_face[ + inner_cells_with_horizontal_faces, 1 + ], + connectivity_cell_to_level_face[inner_cells_with_level_faces, 0], + connectivity_cell_to_level_face[inner_cells_with_level_faces, 1], + ) + ) + mass_matrix_faces_col = np.concatenate( + ( + np.arange(num_faces, dtype=int), + connectivity_cell_to_vertical_face[ + inner_cells_with_vertical_faces, 1 + ], + connectivity_cell_to_vertical_face[ + inner_cells_with_vertical_faces, 0 + ], + connectivity_cell_to_horizontal_face[ + inner_cells_with_horizontal_faces, 1 + ], + connectivity_cell_to_horizontal_face[ + inner_cells_with_horizontal_faces, 0 + ], + connectivity_cell_to_level_face[inner_cells_with_level_faces, 1], + connectivity_cell_to_level_face[inner_cells_with_level_faces, 0], + ) + ) + self.mass_matrix_faces = sps.csc_matrix( + ( + mass_matrix_faces_data, + (mass_matrix_faces_row, mass_matrix_faces_col), + ), + shape=(num_faces, num_faces), + ) + + # Utilities + depth = self.options.get("depth", 0) + self.anderson = ( + darsia.AndersonAcceleration(dimension=num_faces, depth=depth) + if depth > 0 + else None + ) + + # TODO needs to be defined for each problem separately + + # Define sparse embedding operator for fluxes into full discrete DOF space + self.flux_embedding = sps.csc_matrix( + ( + np.ones(num_faces, dtype=float), + (np.arange(num_faces), np.arange(num_faces)), + ), + shape=(num_faces + num_cells + 1, num_faces), + ) + + # Cache + self.num_faces = num_faces + self.num_cells = num_cells + self.dim_cells = dim_cells + self.numbering_cells = numbering_cells + self.num_faces_axis = num_faces_axis + self.vertical_faces_shape = vertical_faces_shape + self.horizontal_faces_shape = horizontal_faces_shape + self.level_faces_shape = level_faces_shape + + def _problem_specific_setup(self, mass_diff: np.ndarray) -> None: + """Resetup of fixed discretization""" + + # TODO can't we just fix some cell, e.g., [0,0]. Move then this to the above. + + # Fix index of dominating contribution in image differece + self.constrained_cell_flat_index = np.argmax(np.abs(mass_diff)) + self.pressure_constraint = sps.csc_matrix( + ( + np.ones(1, dtype=float), + (np.zeros(1, dtype=int), np.array([self.constrained_cell_flat_index])), + ), + shape=(1, self.num_cells), + dtype=float, + ) + + # Linear part of the operator. + self.broken_darcy = sps.bmat( + [ + [None, -self.div.T, None], + [self.div, None, -self.pressure_constraint.T], + [None, self.pressure_constraint, None], + ], + format="csc", + ) + + def split_solution( + self, solution: np.ndarray + ) -> tuple[np.ndarray, np.ndarray, float]: + """Split the solution into fluxes, pressure and lagrange multiplier. + + Args: + solution (np.ndarray): solution + + Returns: + tuple: fluxes, pressure, lagrange multiplier + + """ + # Split the solution + flat_flux = solution[: self.num_faces] + flat_pressure = solution[self.num_faces : self.num_faces + self.num_cells] + flat_lagrange_multiplier = solution[-1] + + return flat_flux, flat_pressure, flat_lagrange_multiplier + + # ! ---- Projections inbetween faces and cells ---- + + def cell_reconstruction(self, flat_flux: np.ndarray) -> np.ndarray: + """Reconstruct the fluxes on the cells from the fluxes on the faces. + + Args: + flat_flux (np.ndarray): flat fluxes (normal fluxes on the faces) + + Returns: + np.ndarray: cell-based vectorial fluxes + + """ + # TODO replace by sparse matrix multiplication + + # Reshape fluxes - use duality of faces and normals + horizontal_fluxes = flat_flux[: self.num_faces_axis[0]].reshape( + self.vertical_faces_shape + ) + vertical_fluxes = flat_flux[ + self.num_faces_axis[0] : self.num_faces_axis[0] + self.num_faces_axis[1] + ].reshape(self.horizontal_faces_shape) + level_fluxes = flat_flux[ + self.num_faces_axis[0] + self.num_faces_axis[1] : + ].reshape(self.level_faces_shape) + + # Determine a cell-based Raviart-Thomas reconstruction of the fluxes + cell_flux = np.zeros((*self.dim_cells, self.dim), dtype=float) + # Horizontal fluxes + cell_flux[:, :-1, :, 0] += 0.5 * horizontal_fluxes + cell_flux[:, 1:, :, 0] += 0.5 * horizontal_fluxes + # Vertical fluxes + cell_flux[:-1, :, :, 1] += 0.5 * vertical_fluxes + cell_flux[1:, :, :, 1] += 0.5 * vertical_fluxes + # Level fluxes + cell_flux[:, :, :-1, 2] += 0.5 * level_fluxes + cell_flux[:, :, 1:, 2] += 0.5 * level_fluxes + + return cell_flux + + def face_restriction(self, cell_flux: np.ndarray) -> np.ndarray: + """Restrict the fluxes on the cells to the faces. + + Args: + cell_flux (np.ndarray): cell-based fluxes + + Returns: + np.ndarray: face-based fluxes + + """ + # TODO replace by sparse matrix multiplication + + # Determine the fluxes on the faces + horizontal_fluxes = 0.5 * (cell_flux[:, :-1, 0] + cell_flux[:, 1:, 0]) + vertical_fluxes = 0.5 * (cell_flux[:-1, :, 1] + cell_flux[1:, :, 1]) + level_fluxes = 0.5 * (cell_flux[:, :, :-1, 2] + cell_flux[:, :, 1:, 2]) + + # Reshape the fluxes + flat_flux = np.concatenate( + [horizontal_fluxes.ravel(), vertical_fluxes.ravel(), level_fluxes.ravel()], + axis=0, + ) + + return flat_flux + + def face_restriction_scalar(self, cell_qty: np.ndarray) -> np.ndarray: + """Restrict the fluxes on the cells to the faces. + + Args: + cell_qty (np.ndarray): cell-based quantity + + Returns: + np.ndarray: face-based quantity + + """ + # Determine the fluxes on the faces + + horizontal_face_qty = 0.5 * (cell_qty[:, :-1, :] + cell_qty[:, 1:, :]) + vertical_face_qty = 0.5 * (cell_qty[:-1, :, :] + cell_qty[1:, :, :]) + level_face_qty = 0.5 * (cell_qty[:, :, :-1] + cell_qty[:, :, 1:]) + + # Reshape the fluxes - hardcoding the connectivity here + face_qty = np.concatenate( + [ + horizontal_face_qty.ravel(), + vertical_face_qty.ravel(), + level_face_qty.ravel(), + ] + ) + + return face_qty + + # ! ---- Effective quantities ---- + + def effective_mobility(self, flat_flux: np.ndarray) -> np.ndarray: + """Compute the effective mobility of the solution. + + Args: + flat_flux (np.ndarray): flat fluxes + + Returns: + np.ndarray: effective mobility + """ + # TODO Use improved quadrature? + cell_flux = self.cell_reconstruction(flat_flux) + return np.linalg.norm(cell_flux, 2, axis=-1) + + def l1_dissipation(self, solution: np.ndarray) -> float: + """Compute the l1 dissipation potential of the solution. + + Args: + solution (np.ndarray): solution + + Returns: + float: l1 dissipation potential + + """ + # TODO use improved quadrature? + flat_flux, _, _ = self.split_solution(solution) + cell_flux = self.cell_reconstruction(flat_flux) + return np.sum(np.prod(self.voxel_size) * np.linalg.norm(cell_flux, 2, axis=-1)) + + # ! ---- Main methods ---- + + def __call__( + self, + img_1: darsia.Image, + img_2: darsia.Image, + plot_solution: bool = False, + return_solution: bool = False, + ) -> float: + """L1 Wasserstein distance for two images with same mass. + + NOTE: Images need to comply with the setup of the object. + + Args: + img_1 (darsia.Image): image 1 + img_2 (darsia.Image): image 2 + plot_solution (bool): plot the solution. Defaults to False. + return_solution (bool): return the solution. Defaults to False. + + Returns: + float or array: distance between img_1 and img_2. + + """ + + # Start taking time + tic = time.time() + + # Compatibilty check + assert img_1.scalar and img_2.scalar + self._compatibility_check_3d(img_1, img_2) + + # Determine difference of distriutions and define corresponding rhs + mass_diff = img_1.img - img_2.img + flat_mass_diff = np.ravel(mass_diff) + self._problem_specific_setup(mass_diff) + + print("Finished: problem specific setup") + + # Main method + distance, solution, status = self._solve(flat_mass_diff) + + print("finished: solve") + + # Split the solution + flat_flux, flat_pressure, _ = self.split_solution(solution) + + print("finished: split") + + # Reshape the fluxes and pressure + flux = self.cell_reconstruction(flat_flux) + pressure = flat_pressure.reshape(self.dim_cells) + + print("finished: cell reconstruction") + + # Determine effective mobility + mobility = self.effective_mobility(flat_flux) + + print("finished: effective mobility") + + # Stop taking time + toc = time.time() + status["elapsed_time"] = toc - tic + + # TODO consider suitable visualization? +# # Plot the solution +# if plot_solution: +# self._plot_solution(mass_diff, flux, pressure, mobility) + + if return_solution: + return distance, flux, pressure, mobility, status + else: + return distance + + def _compatibility_check_3d( + self, + img_1: darsia.Image, + img_2: darsia.Image, + ) -> bool: + """ + Compatibility check. + + Args: + img_1 (Image): image 1 + img_2 (Image): image 2 + + Returns: + bool: flag whether images 1 and 2 can be compared. + + """ + # Scalar valued + assert img_1.scalar and img_2.scalar + + # Series + assert img_1.time_num == img_2.time_num + + # Two-dimensional + assert img_1.space_dim == 3 and img_2.space_dim == 3 + + # Check whether the coordinate system is compatible + assert darsia.check_equal_coordinatesystems( + img_1.coordinatesystem, img_2.coordinatesystem + ) + assert np.allclose(img_1.voxel_size, img_2.voxel_size) + + # Compatible distributions - comparing sums is sufficient since it is implicitly + # assumed that the coordinate systems are equivalent. Check each time step + # separately. + assert np.allclose(self._sum(img_1), self._sum(img_2)) + + def _plot_solution( + self, + mass_diff: np.ndarray, + flux: np.ndarray, + pressure: np.ndarray, + mobility: np.ndarray, + ) -> None: + assert False + + +class WassersteinDistanceNewton3d(VariationalWassersteinDistance3d): + """Class to determine the L1 EMD/Wasserstein distance solved with Newton's method.""" + + def residual(self, rhs: np.ndarray, solution: np.ndarray) -> np.ndarray: + """Compute the residual of the solution. + + Args: + rhs (np.ndarray): right hand side + solution (np.ndarray): solution + + Returns: + np.ndarray: residual + + """ + flat_flux, _, _ = self.split_solution(solution) + cell_flux = self.cell_reconstruction(flat_flux) + cell_flux_norm = np.maximum( + np.linalg.norm(cell_flux, 2, axis=-1), self.regularization + ) + cell_flux_normed = cell_flux / cell_flux_norm[..., None] + flat_flux_normed = self.face_restriction(cell_flux_normed) + return ( + rhs + - self.broken_darcy.dot(solution) + - self.flux_embedding.dot(self.mass_matrix_faces.dot(flat_flux_normed)) + ) + + def jacobian_lu(self, solution: np.ndarray) -> sps.linalg.splu: + """Compute the LU factorization of the jacobian of the solution. + + Args: + solution (np.ndarray): solution + + Returns: + sps.linalg.splu: LU factorization of the jacobian + + """ + flat_flux, _, _ = self.split_solution(solution) + cell_flux = self.cell_reconstruction(flat_flux) + self.regularization = self.options.get("regularization", 0.0) + cell_flux_norm = np.maximum( + np.linalg.norm(cell_flux, 2, axis=-1), self.regularization + ) + flat_flux_norm = self.face_restriction_scalar(cell_flux_norm) + approx_jacobian = sps.bmat( + [ + [ + sps.diags(np.maximum(self.L, 1.0 / flat_flux_norm), dtype=float) + * self.mass_matrix_faces, + -self.div.T, + None, + ], + [self.div, None, -self.pressure_constraint.T], + [None, self.pressure_constraint, None], + ], + format="csc", + ) + approx_jacobian_lu = sps.linalg.splu(approx_jacobian) + return approx_jacobian_lu + + def _solve(self, flat_mass_diff): + # Observation: AA can lead to less stagnation, more accurate results, and therefore + # better solutions to mu and u. Higher depth is better, but more expensive. + + # Solver parameters + num_iter = self.options.get("num_iter", 100) + tol = self.options.get("tol", 1e-6) + tol_distance = self.options.get("tol_distance", 1e-6) + self.L = self.options.get("L", 1.0) + + # Define right hand side + rhs = np.concatenate( + [ + np.zeros(self.num_faces, dtype=float), + self.mass_matrix_cells.dot(flat_mass_diff), + np.zeros(1, dtype=float), + ] + ) + + # Initialize solution + solution_i = np.zeros_like(rhs) + + # Newton iteration + for iter in range(num_iter): + # Keep track of old flux, and old distance + old_solution_i = solution_i.copy() + old_distance = self.l1_dissipation(solution_i) + + # Newton step + if iter == 0: + residual_i = ( + rhs.copy() + ) # Aim at Darcy-like initial guess after first iteration. + else: + residual_i = self.residual(rhs, solution_i) + jacobian_lu = self.jacobian_lu(solution_i) + update_i = jacobian_lu.solve(residual_i) + solution_i += update_i + + # Apply Anderson acceleration to flux contribution (the only nonlinear part). + if self.anderson is not None: + solution_i[: self.num_faces] = self.anderson( + solution_i[: self.num_faces], update_i[: self.num_faces], iter + ) + # TODO try for full solution + + # Update distance + new_distance = self.l1_dissipation(solution_i) + + # Compute the error: + # - residual + # - residual of mass conservation equation + # - increment + # - flux increment + error = [ + np.linalg.norm(residual_i, 2), + np.linalg.norm(residual_i[self.num_faces : -1], 2), + np.linalg.norm(solution_i - old_solution_i, 2), + np.linalg.norm((solution_i - old_solution_i)[: self.num_faces], 2), + ] + + if self.verbose: + print( + "Newton iteration", + iter, + new_distance, + old_distance - new_distance, + error[0], # residual + error[1], # mass conservation residual + error[2], # full increment + error[3], # flux increment + ) + + # Stopping criterion + # TODO include criterion build on staganation of the solution + # TODO include criterion on distance. + if ( + iter > 1 + and min([error[0], error[2]]) < tol + or abs(new_distance - old_distance) < tol_distance + ): + break + + # Define performance metric + status = { + "converged": iter < num_iter, + "number iterations": iter, + "distance": new_distance, + "residual": error[0], + "mass conservation residual": error[1], + "increment": error[2], + "flux increment": error[3], + "distance increment": abs(new_distance - old_distance), + } + + return new_distance, solution_i, status + + +class WassersteinDistanceBregman3d(VariationalWassersteinDistance3d): + def _problem_specific_setup(self, mass_diff: np.ndarray) -> None: + super()._problem_specific_setup(mass_diff) + self.L = self.options.get("L", 1.0) + l_scheme_mixed_darcy = sps.bmat( + [ + [self.L * self.mass_matrix_faces, -self.div.T, None], + [self.div, None, -self.pressure_constraint.T], + [None, self.pressure_constraint, None], + ], + format="csc", + ) + self.l_scheme_mixed_darcy_lu = sps.linalg.splu(l_scheme_mixed_darcy) + + def _solve(self, flat_mass_diff): + # Solver parameters + num_iter = self.options.get("num_iter", 100) + # tol = self.options.get("tol", 1e-6) # TODO make use of tol, or remove + self.L = self.options.get("L", 1.0) + + rhs = np.concatenate( + [ + np.zeros(self.num_faces, dtype=float), + self.mass_matrix_cells.dot(flat_mass_diff), + np.zeros(1, dtype=float), + ] + ) + + # Keep track of how often the distance increases. + num_neg_diff = 0 + + # Bregman iterations + solution_i = np.zeros_like(rhs) + for iter in range(num_iter): + old_distance = self.l1_dissipation(solution_i) + + # 1. Solve linear system with trust in current flux. + flat_flux_i, _, _ = self.split_solution(solution_i) + rhs_i = rhs.copy() + rhs_i[: self.num_faces] = self.L * self.mass_matrix_faces.dot(flat_flux_i) + intermediate_solution_i = self.l_scheme_mixed_darcy_lu.solve(rhs_i) + + # 2. Shrink step for vectorial fluxes. To comply with the RT0 setting, the + # shrinkage operation merely determines the scalar. We still aim at + # following along the direction provided by the vectorial fluxes. + intermediate_flat_flux_i, _, _ = self.split_solution( + intermediate_solution_i + ) + # new_flat_flux_i = np.sign(intermediate_flat_flux_i) * ( + # np.maximum(np.abs(intermediate_flat_flux_i) - 1.0 / self.L, 0.0) + # ) + cell_intermediate_flux_i = self.cell_reconstruction( + intermediate_flat_flux_i + ) + norm = np.linalg.norm(cell_intermediate_flux_i, 2, axis=-1) + cell_scaling = np.maximum(norm - 1 / self.L, 0) / ( + norm + self.regularization + ) + flat_scaling = self.face_restriction_scalar(cell_scaling) + new_flat_flux_i = flat_scaling * intermediate_flat_flux_i + + # Apply Anderson acceleration to flux contribution (the only nonlinear part). + if self.anderson is not None: + flux_inc = new_flat_flux_i - flat_flux_i + new_flat_flux_i = self.anderson(new_flat_flux_i, flux_inc, iter) + + # Measure error in terms of the increment of the flux + flux_diff = np.linalg.norm(new_flat_flux_i - flat_flux_i, 2) + + # Update flux solution + solution_i = intermediate_solution_i.copy() + solution_i[: self.num_faces] = new_flat_flux_i + + # Update distance + new_distance = self.l1_dissipation(solution_i) + + # Determine the error in the mass conservation equation + mass_conservation_residual = np.linalg.norm( + (rhs_i - self.broken_darcy.dot(solution_i))[self.num_faces : -1], 2 + ) + + # TODO include criterion build on staganation of the solution + # TODO include criterion on distance. + + # Print status + if self.verbose: + print( + "Bregman iteration", + iter, + new_distance, + old_distance - new_distance, + self.L, + flux_diff, + mass_conservation_residual, + ) + + ## Check stopping criterion # TODO. What is a good stopping criterion? + # if iter > 1 and (flux_diff < tol or mass_conservation_residual < tol: + # break + + # Keep track if the distance increases. + if new_distance > old_distance: + num_neg_diff += 1 + + # Increase L if stagnating of the distance increases too often. + # TODO restart anderson acceleration + update_l = self.options.get("update_l", True) + if update_l: + tol_distance = self.options.get("tol_distance", 1e-12) + max_iter_increase_diff = self.options.get("max_iter_increase_diff", 20) + l_factor = self.options.get("l_factor", 2) + if ( + abs(new_distance - old_distance) < tol_distance + or num_neg_diff > max_iter_increase_diff + ): + # Update L + self.L = self.L * l_factor + + # Update linear system + l_scheme_mixed_darcy = sps.bmat( + [ + [self.L * self.mass_matrix_faces, -self.div.T, None], + [self.div, None, -self.pressure_constraint.T], + [None, self.pressure_constraint, None], + ], + format="csc", + ) + self.l_scheme_mixed_darcy_lu = sps.linalg.splu(l_scheme_mixed_darcy) + + # Reset stagnation counter + num_neg_diff = 0 + + L_max = self.options.get("L_max", 1e8) + if self.L > L_max: + break + + # Define performance metric + status = { + "converged": iter < num_iter, + "number iterations": iter, + "distance": new_distance, + "residual mass conservation": mass_conservation_residual, + "flux increment": flux_diff, + "distance increment": abs(new_distance - old_distance), + } + + return new_distance, solution_i, status + + +# Unified access +def wasserstein_distance_3d( + mass_1: darsia.Image, + mass_2: darsia.Image, + method: str, + **kwargs, +): + """Unified access to Wasserstein distance computation between images with same mass. + + Args: + mass_1 (darsia.Image): image 1 + mass_2 (darsia.Image): image 2 + method (str): method to use ("newton", "bregman", or "cv2.emd") + **kwargs: additional arguments (only for "newton" and "bregman") + - options (dict): options for the method. + - plot_solution (bool): plot the solution. Defaults to False. + - return_solution (bool): return the solution. Defaults to False. + + """ + if method.lower() in ["newton", "bregman"]: + shape = mass_1.img.shape + voxel_size = mass_1.voxel_size + dim = mass_1.space_dim + plot_solution = kwargs.get("plot_solution", False) + return_solution = kwargs.get("return_solution", False) + options = kwargs.get("options", {}) + options["name"] = kwargs.get("name") + + if method.lower() == "newton": + w1 = WassersteinDistanceNewton3d(shape, voxel_size, dim, options) + elif method.lower() == "bregman": + w1 = WassersteinDistanceBregman3d(shape, voxel_size, dim, options) + return w1( + mass_1, mass_2, plot_solution=plot_solution, return_solution=return_solution + ) + + elif method.lower() == "cv2.emd": + preprocess = kwargs.get("preprocess") + w1 = darsia.EMD(preprocess) + return w1(mass_1, mass_2) + + else: + raise NotImplementedError(f"Method {method} not implemented.") From 7bf82611ebaa312a934c4a514ab7c00c64df14fa Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Mon, 28 Aug 2023 21:34:39 +0200 Subject: [PATCH 007/100] ENH: Add restart and adaptive dimension to AA. --- src/darsia/utils/andersonacceleration.py | 54 +++++++++++++++++------- 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/src/darsia/utils/andersonacceleration.py b/src/darsia/utils/andersonacceleration.py index e6b7eaf7..0f98e406 100644 --- a/src/darsia/utils/andersonacceleration.py +++ b/src/darsia/utils/andersonacceleration.py @@ -1,14 +1,30 @@ -from typing import Union +"""Anderson acceleration as described by Walker and Ni in doi:10.2307/23074353.""" + +from typing import Optional, Union import numpy as np import scipy as sp class AndersonAcceleration: - """Anderson acceleration as described by Walker and Ni in doi:10.2307/23074353.""" + """Anderson acceleration for fixed-point methods.""" + + def __init__( + self, + dimension: Optional[Union[int, tuple[int]]] = None, + depth: int = 0, + restart: Optional[int] = None, + ) -> None: + """Initialize Anderson acceleration. - def __init__(self, dimension: Union[int, tuple[int]], depth: int = 0) -> None: - """Initialize Anderson acceleration.""" + Args: + dimension (int, tuple[int]): dimension of the problem. If a tuple is given, + the problem is assumed to be a tensor problem and the dimension is + calculated as the product of the tuple entries. + depth (int): depth of the acceleration. If 0, no acceleration is applied. + restart (int): restart of the acceleration. If None, no restart is applied. + + """ if isinstance(dimension, np.integer): self._dimension = dimension @@ -17,15 +33,14 @@ def __init__(self, dimension: Union[int, tuple[int]], depth: int = 0) -> None: self.tensor_shape = dimension self._dimension = int(np.prod(dimension)) self._tensor = True + elif dimension is None: + self._tensor = False + pass else: raise ValueError("Dimension not recognized.") self._depth = depth - - # Initialize arrays for iterates. - self.reset() - self._fkm1: np.ndarray = self._Fk.copy() - self._gkm1: np.ndarray = self._Gk.copy() + self._restart = restart def reset(self) -> None: """Reset Anderson acceleration.""" @@ -35,6 +50,10 @@ def reset(self) -> None: self._Gk: np.ndarray = np.zeros( (self._dimension, self._depth) ) # changes in fixed point applications + self._fkm1: np.ndarray = self._Fk.copy() + self._gkm1: np.ndarray = self._Gk.copy() + + self._inner_iteration = 0 def __call__(self, gk: np.ndarray, fk: np.ndarray, iteration: int) -> np.ndarray: """Apply Anderson acceleration. @@ -54,15 +73,18 @@ def __call__(self, gk: np.ndarray, fk: np.ndarray, iteration: int) -> np.ndarray gk = np.ravel(gk) fk = np.ravel(fk) - if iteration == 0: - self._Fk = np.zeros((self._dimension, self._depth)) # changes in increments - self._Gk = np.zeros( - (self._dimension, self._depth) - ) # changes in fixed point applications + if self._restart is not None: + self._inner_iteration = iteration % self._restart + else: + self._inner_iteration = iteration - mk = min(iteration, self._depth) + # Reset if necessary. + if self._inner_iteration == 0: + self._dimension = len(gk) + self.reset() - # Apply actual acceleration (not in the first iteration). + # Apply actual acceleration (not in the first iteration, of any restart loop). + mk = min(self._inner_iteration, self._depth) if mk > 0: # Build matrices of changes col = (iteration - 1) % self._depth From daf5932d4009edca414f51dc08768f703d8e1902 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Mon, 28 Aug 2023 21:36:38 +0200 Subject: [PATCH 008/100] ENH: Add possibility to use restarted AA. --- src/darsia/measure/wasserstein.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index a12aa307..aaa75797 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -47,7 +47,8 @@ def __init__( - L (float): parameter for the Bregman iteration. Defaults to 1.0. - regularization (float): regularization parameter for the Bregman iteration. Defaults to 0.0. - - depth (int): depth of the Anderson acceleration. Defaults to 0. + - aa_depth (int): depth of the Anderson acceleration. Defaults to 0. + - aa_restart (int): restart of the Anderson acceleration. Defaults to None. - scaling (float): scaling of the fluxes in the plot. Defaults to 1.0. - lumping (bool): lump the mass matrix. Defaults to True. @@ -234,10 +235,13 @@ def _setup(self) -> None: ) # Utilities - depth = self.options.get("depth", 0) + aa_depth = self.options.get("aa_depth", 0) + aa_restart = self.options.get("aa_restart", None) self.anderson = ( - darsia.AndersonAcceleration(dimension=num_edges, depth=depth) - if depth > 0 + darsia.AndersonAcceleration( + dimension=None, depth=aa_depth, restart=aa_restart + ) + if aa_depth > 0 else None ) From 6dc2d0192044c4ca7da06aa3be391837db61cdb5 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Mon, 28 Aug 2023 21:38:57 +0200 Subject: [PATCH 009/100] MAINT: Rename variables. --- src/darsia/measure/wasserstein.py | 119 ++++++++++++++++-------------- 1 file changed, 64 insertions(+), 55 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index aaa75797..81d6a581 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -272,7 +272,7 @@ def _problem_specific_setup(self, mass_diff: np.ndarray) -> None: # Fix index of dominating contribution in image differece self.constrained_cell_flat_index = np.argmax(np.abs(mass_diff)) - self.pressure_constraint = sps.csc_matrix( + self.potential_constraint = sps.csc_matrix( ( np.ones(1, dtype=float), (np.zeros(1, dtype=int), np.array([self.constrained_cell_flat_index])), @@ -285,8 +285,8 @@ def _problem_specific_setup(self, mass_diff: np.ndarray) -> None: self.broken_darcy = sps.bmat( [ [None, -self.div.T, None], - [self.div, None, -self.pressure_constraint.T], - [None, self.pressure_constraint, None], + [self.div, None, -self.potential_constraint.T], + [None, self.potential_constraint, None], ], format="csc", ) @@ -294,21 +294,21 @@ def _problem_specific_setup(self, mass_diff: np.ndarray) -> None: def split_solution( self, solution: np.ndarray ) -> tuple[np.ndarray, np.ndarray, float]: - """Split the solution into fluxes, pressure and lagrange multiplier. + """Split the solution into fluxes, potential and lagrange multiplier. Args: solution (np.ndarray): solution Returns: - tuple: fluxes, pressure, lagrange multiplier + tuple: fluxes, potential, lagrange multiplier """ # Split the solution flat_flux = solution[: self.num_edges] - flat_pressure = solution[self.num_edges : self.num_edges + self.num_cells] + flat_potential = solution[self.num_edges : self.num_edges + self.num_cells] flat_lagrange_multiplier = solution[-1] - return flat_flux, flat_pressure, flat_lagrange_multiplier + return flat_flux, flat_potential, flat_lagrange_multiplier # ! ---- Projections inbetween faces and cells ---- @@ -390,17 +390,15 @@ def face_restriction_scalar(self, cell_qty: np.ndarray) -> np.ndarray: # ! ---- Effective quantities ---- - def effective_mobility(self, flat_flux: np.ndarray) -> np.ndarray: - """Compute the effective mobility of the solution. + def transport_density(self, cell_flux: np.ndarray) -> np.ndarray: + """Compute the transport density of the solution. Args: flat_flux (np.ndarray): flat fluxes Returns: - np.ndarray: effective mobility + np.ndarray: transport density """ - # TODO Use improved quadrature? - cell_flux = self.cell_reconstruction(flat_flux) return np.linalg.norm(cell_flux, 2, axis=-1) def l1_dissipation(self, solution: np.ndarray) -> float: @@ -458,14 +456,14 @@ def __call__( distance, solution, status = self._solve(flat_mass_diff) # Split the solution - flat_flux, flat_pressure, _ = self.split_solution(solution) + flat_flux, flat_potential, _ = self.split_solution(solution) - # Reshape the fluxes and pressure + # Reshape the fluxes and potential to grid format flux = self.cell_reconstruction(flat_flux) - pressure = flat_pressure.reshape(self.dim_cells) + potential = flat_potential.reshape(self.dim_cells) - # Determine effective mobility - mobility = self.effective_mobility(flat_flux) + # Determine transport density + transport_density = self.transport_density(flux) # Stop taking time toc = time.time() @@ -473,10 +471,10 @@ def __call__( # Plot the solution if plot_solution: - self._plot_solution(mass_diff, flux, pressure, mobility) + self._plot_solution(mass_diff, flux, potential, transport_density) if return_solution: - return distance, flux, pressure, mobility, status + return distance, flux, potential, transport_density, status else: return distance @@ -484,8 +482,8 @@ def _plot_solution( self, mass_diff: np.ndarray, flux: np.ndarray, - pressure: np.ndarray, - mobility: np.ndarray, + potential: np.ndarray, + transport_density: np.ndarray, ) -> None: # Meshgrid Y, X = np.meshgrid( @@ -494,19 +492,15 @@ def _plot_solution( indexing="ij", ) - # Control of flux arrows - scaling = self.options.get("scaling", 1.0) - frequency = self.options.get("plot_frequency", 1) - - # Plot the fluxes and pressure - plt.figure("Beckman pressure solution") - plt.pcolormesh(X, Y, pressure, cmap="turbo") - plt.colorbar(label="pressure") + # Plot the potential + plt.figure("Beckman solution potential") + plt.pcolormesh(X, Y, potential, cmap="turbo") + plt.colorbar(label="potential") plt.quiver( - X[::frequency, ::frequency], - Y[::frequency, ::frequency], - scaling * flux[::frequency, ::frequency, 0], - -scaling * flux[::frequency, ::frequency, 1], + X[::resolution, ::resolution], + Y[::resolution, ::resolution], + scaling * flux[::resolution, ::resolution, 0], + -scaling * flux[::resolution, ::resolution, 1], angles="xy", scale_units="xy", scale=1, @@ -515,22 +509,23 @@ def _plot_solution( ) plt.xlabel("x [m]") plt.ylabel("y [m]") - plt.ylim(top=0.08) - if self.options["name"] is not None: + plt.ylim(top=0.08) # TODO rm? + if save_plot: plt.savefig( - self.options["name"] + "_beckman_pressure_solution.png", + folder + "/" + name + "_beckman_solution_potential.png", dpi=500, transparent=True, ) + # Plot the fluxes plt.figure("Beckman solution fluxes") - plt.pcolormesh(X, Y, mass_diff, cmap="turbo") + plt.pcolormesh(X, Y, mass_diff, cmap="turbo", vmin=-1, vmax=3.5) plt.colorbar(label="mass difference") plt.quiver( - X[::frequency, ::frequency], - Y[::frequency, ::frequency], - scaling * flux[::frequency, ::frequency, 0], - -scaling * flux[::frequency, ::frequency, 1], + X[::resolution, ::resolution], + Y[::resolution, ::resolution], + scaling * flux[::resolution, ::resolution, 0], + -scaling * flux[::resolution, ::resolution, 1], angles="xy", scale_units="xy", scale=1, @@ -540,27 +535,40 @@ def _plot_solution( plt.xlabel("x [m]") plt.ylabel("y [m]") plt.ylim(top=0.08) - if self.options["name"] is not None: + plt.text( + 0.0025, + 0.075, + name, + color="white", + alpha=0.9, + rotation=0, + fontsize=14, + ) # TODO rm? + if save_plot: plt.savefig( - self.options["name"] + "_beckman_solution_fluxes.png", + folder + "/" + name + "_beckman_solution_fluxes.png", dpi=500, transparent=True, ) - plt.figure("Beckman solution mobility") - plt.pcolormesh(X, Y, mobility, cmap="turbo") + # Plot the transport density + plt.figure("L1 optimal transport density") + plt.pcolormesh(X, Y, transport_density, cmap="turbo") plt.colorbar(label="flux modulus") plt.xlabel("x [m]") plt.ylabel("y [m]") - plt.ylim(top=0.08) - if self.options["name"] is not None: + plt.ylim(top=0.08) # TODO rm? + if save_plot: plt.savefig( - self.options["name"] + "_beckman_solution_mobility.png", + folder + "/" + name + "_beckman_solution_transport_density.png", dpi=500, transparent=True, ) - plt.show() + if show_plot: + plt.show() + else: + plt.close("all") class WassersteinDistanceNewton(VariationalWassersteinDistance): @@ -728,8 +736,8 @@ def _problem_specific_setup(self, mass_diff: np.ndarray) -> None: l_scheme_mixed_darcy = sps.bmat( [ [self.L * self.mass_matrix_edges, -self.div.T, None], - [self.div, None, -self.pressure_constraint.T], - [None, self.pressure_constraint, None], + [self.div, None, -self.potential_constraint.T], + [None, self.potential_constraint, None], ], format="csc", ) @@ -843,8 +851,8 @@ def _solve(self, flat_mass_diff): l_scheme_mixed_darcy = sps.bmat( [ [self.L * self.mass_matrix_edges, -self.div.T, None], - [self.div, None, -self.pressure_constraint.T], - [None, self.pressure_constraint, None], + [self.div, None, -self.potential_constraint.T], + [None, self.potential_constraint, None], ], format="csc", ) @@ -893,10 +901,11 @@ def wasserstein_distance( shape = mass_1.img.shape voxel_size = mass_1.voxel_size dim = mass_1.space_dim + + # Fetch options + options = kwargs.get("options", {}) plot_solution = kwargs.get("plot_solution", False) return_solution = kwargs.get("return_solution", False) - options = kwargs.get("options", {}) - options["name"] = kwargs.get("name") if method.lower() == "newton": w1 = WassersteinDistanceNewton(shape, voxel_size, dim, options) From 712c49c79c538df156fd0c02d718bfe0fc84a485 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Mon, 28 Aug 2023 21:41:25 +0200 Subject: [PATCH 010/100] ENH: Extend plotting capabilities and convergence monitoring. --- src/darsia/measure/wasserstein.py | 94 +++++++++++++++++++++++++++---- 1 file changed, 82 insertions(+), 12 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index 81d6a581..88be5c1f 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -4,6 +4,7 @@ from __future__ import annotations import time +from pathlib import Path import matplotlib.pyplot as plt import numpy as np @@ -485,6 +486,30 @@ def _plot_solution( potential: np.ndarray, transport_density: np.ndarray, ) -> None: + """Plot the solution. + + Args: + mass_diff (np.ndarray): difference of mass distributions + flux (np.ndarray): fluxes + potential (np.ndarray): potential + transport_density (np.ndarray): transport density + + """ + # Fetch options + plot_options = self.options.get("plot_options", {}) + + # Store plot + save_plot = plot_options.get("save", False) + if save_plot: + name = plot_options.get("name", None) + folder = plot_options.get("folder", ".") + Path(folder).mkdir(parents=True, exist_ok=True) + show_plot = plot_options.get("show", True) + + # Control of flux arrows + scaling = plot_options.get("scaling", 1.0) + resolution = plot_options.get("resolution", 1) + # Meshgrid Y, X = np.meshgrid( self.voxel_size[0] * (0.5 + np.arange(self.shape[0] - 1, -1, -1)), @@ -623,22 +648,34 @@ def jacobian_lu(self, solution: np.ndarray) -> sps.linalg.splu: -self.div.T, None, ], - [self.div, None, -self.pressure_constraint.T], - [None, self.pressure_constraint, None], + [self.div, None, -self.potential_constraint.T], + [None, self.potential_constraint, None], ], format="csc", ) approx_jacobian_lu = sps.linalg.splu(approx_jacobian) return approx_jacobian_lu - def _solve(self, flat_mass_diff): - # Observation: AA can lead to less stagnation, more accurate results, and therefore + def _solve(self, flat_mass_diff: np.ndarray) -> tuple[float, np.ndarray, dict]: + """Solve the Beckman problem using Newton's method. + + Args: + flat_mass_diff (np.ndarray): difference of mass distributions + + Returns: + tuple: distance, solution, status + + """ + # TODO rm: Observation: AA can lead to less stagnation, more accurate results, and therefore # better solutions to mu and u. Higher depth is better, but more expensive. # Solver parameters num_iter = self.options.get("num_iter", 100) - tol = self.options.get("tol", 1e-6) + tol_residual = self.options.get("tol_residual", 1e-6) + tol_increment = self.options.get("tol_increment", 1e-6) tol_distance = self.options.get("tol_distance", 1e-6) + + # Relaxation parameter self.L = self.options.get("L", 1.0) # Define right hand side @@ -653,6 +690,29 @@ def _solve(self, flat_mass_diff): # Initialize solution solution_i = np.zeros_like(rhs) + # Initialize container for storing the convergence history + convergence_history = { + "distance": [], + "residual": [], + "mass conservation residual": [], + "increment": [], + "flux increment": [], + "distance increment": [], + } + + # Print header + if self.verbose: + print( + "--- ; ", + "Newton iteration", + "distance", + "residual", + "mass conservation residual", + "increment", + "flux increment", + "distance increment", + ) + # Newton iteration for iter in range(num_iter): # Keep track of old flux, and old distance @@ -671,11 +731,12 @@ def _solve(self, flat_mass_diff): solution_i += update_i # Apply Anderson acceleration to flux contribution (the only nonlinear part). + # Application to full solution, or just the potential, lead to divergence, + # while application to the flux, results in improved performance. if self.anderson is not None: solution_i[: self.num_edges] = self.anderson( solution_i[: self.num_edges], update_i[: self.num_edges], iter ) - # TODO try for full solution # Update distance new_distance = self.l1_dissipation(solution_i) @@ -685,32 +746,40 @@ def _solve(self, flat_mass_diff): # - residual of mass conservation equation # - increment # - flux increment + # - distance increment error = [ np.linalg.norm(residual_i, 2), np.linalg.norm(residual_i[self.num_edges : -1], 2), np.linalg.norm(solution_i - old_solution_i, 2), np.linalg.norm((solution_i - old_solution_i)[: self.num_edges], 2), + abs(new_distance - old_distance), ] + # Update convergence history + convergence_history["distance"].append(new_distance) + convergence_history["residual"].append(error[0]) + convergence_history["mass conservation residual"].append(error[1]) + convergence_history["increment"].append(error[2]) + convergence_history["flux increment"].append(error[3]) + convergence_history["distance increment"].append(error[4]) + if self.verbose: print( "Newton iteration", iter, new_distance, - old_distance - new_distance, error[0], # residual error[1], # mass conservation residual error[2], # full increment error[3], # flux increment + error[4], # distance increment ) # Stopping criterion # TODO include criterion build on staganation of the solution - # TODO include criterion on distance. - if ( - iter > 1 - and min([error[0], error[2]]) < tol - or abs(new_distance - old_distance) < tol_distance + if iter > 1 and ( + (error[0] < tol_residual and error[2] < tol_increment) + or error[4] < tol_distance ): break @@ -724,6 +793,7 @@ def _solve(self, flat_mass_diff): "increment": error[2], "flux increment": error[3], "distance increment": abs(new_distance - old_distance), + "convergence history": convergence_history, } return new_distance, solution_i, status From e5e13f5f92a34d317146b92f0bd60c33ae0ebc73 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Mon, 28 Aug 2023 21:53:33 +0200 Subject: [PATCH 011/100] MAINT: rename edge -> face. --- src/darsia/measure/wasserstein.py | 136 +++++++++++++++--------------- 1 file changed, 68 insertions(+), 68 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index 88be5c1f..9873055c 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -77,50 +77,50 @@ def _setup(self) -> None: num_cells = np.prod(dim_cells) numbering_cells = np.arange(num_cells, dtype=int).reshape(dim_cells) - # Consider only inner edges - vertical_edges_shape = (self.shape[0], self.shape[1] - 1) - horizontal_edges_shape = (self.shape[0] - 1, self.shape[1]) - num_edges_axis = [ - np.prod(vertical_edges_shape), - np.prod(horizontal_edges_shape), + # Consider only inner faces + vertical_faces_shape = (self.shape[0], self.shape[1] - 1) + horizontal_faces_shape = (self.shape[0] - 1, self.shape[1]) + num_faces_axis = [ + np.prod(vertical_faces_shape), + np.prod(horizontal_faces_shape), ] - num_edges = np.sum(num_edges_axis) + num_faces = np.sum(num_faces_axis) - # Define connectivity - connectivity = np.zeros((num_edges, 2), dtype=int) - connectivity[: num_edges_axis[0], 0] = np.ravel( + # Define connectivity and direction of the normal on faces + connectivity = np.zeros((num_faces, 2), dtype=int) + connectivity[: num_faces_axis[0], 0] = np.ravel( numbering_cells[:, :-1] ) # left cells - connectivity[: num_edges_axis[0], 1] = np.ravel( + connectivity[: num_faces_axis[0], 1] = np.ravel( numbering_cells[:, 1:] ) # right cells - connectivity[num_edges_axis[0] :, 0] = np.ravel( + connectivity[num_faces_axis[0] :, 0] = np.ravel( numbering_cells[:-1, :] ) # top cells - connectivity[num_edges_axis[0] :, 1] = np.ravel( + connectivity[num_faces_axis[0] :, 1] = np.ravel( numbering_cells[1:, :] ) # bottom cells # Define sparse divergence operator, integrated over elements: flat_fluxes -> flat_mass div_data = np.concatenate( ( - self.voxel_size[0] * np.ones(num_edges_axis[0], dtype=float), - self.voxel_size[1] * np.ones(num_edges_axis[1], dtype=float), - -self.voxel_size[0] * np.ones(num_edges_axis[0], dtype=float), - -self.voxel_size[1] * np.ones(num_edges_axis[1], dtype=float), + self.voxel_size[0] * np.ones(num_faces_axis[0], dtype=float), + self.voxel_size[1] * np.ones(num_faces_axis[1], dtype=float), + -self.voxel_size[0] * np.ones(num_faces_axis[0], dtype=float), + -self.voxel_size[1] * np.ones(num_faces_axis[1], dtype=float), ) ) div_row = np.concatenate( ( - connectivity[: num_edges_axis[0], 0], - connectivity[num_edges_axis[0] :, 0], - connectivity[: num_edges_axis[0], 1], - connectivity[num_edges_axis[0] :, 1], + connectivity[: num_faces_axis[0], 0], + connectivity[num_faces_axis[0] :, 0], + connectivity[: num_faces_axis[0], 1], + connectivity[num_faces_axis[0] :, 1], ) ) - div_col = np.tile(np.arange(num_edges, dtype=int), 2) + div_col = np.tile(np.arange(num_faces, dtype=int), 2) self.div = sps.csc_matrix( - (div_data, (div_row, div_col)), shape=(num_cells, num_edges) + (div_data, (div_row, div_col)), shape=(num_cells, num_faces) ) # Define sparse mass matrix on cells: flat_mass -> flat_mass @@ -128,11 +128,11 @@ def _setup(self) -> None: np.prod(self.voxel_size) * np.ones(num_cells, dtype=float) ) - # Define sparse mass matrix on edges: flat_fluxes -> flat_fluxes + # Define sparse mass matrix on faces: flat_fluxes -> flat_fluxes lumping = self.options.get("lumping", True) if lumping: - self.mass_matrix_edges = sps.diags( - np.prod(self.voxel_size) * np.ones(num_edges, dtype=float) + self.mass_matrix_faces = sps.diags( + np.prod(self.voxel_size) * np.ones(num_faces, dtype=float) ) else: # Define connectivity: cell to face (only for inner cells) @@ -140,23 +140,23 @@ def _setup(self) -> None: connectivity_cell_to_vertical_face[ np.ravel(numbering_cells[:, :-1]), 0 ] = np.arange( - num_edges_axis[0] + num_faces_axis[0] ) # left face connectivity_cell_to_vertical_face[ np.ravel(numbering_cells[:, 1:]), 1 ] = np.arange( - num_edges_axis[0] + num_faces_axis[0] ) # right face connectivity_cell_to_horizontal_face = np.zeros((num_cells, 2), dtype=int) connectivity_cell_to_horizontal_face[ np.ravel(numbering_cells[:-1, :]), 0 ] = np.arange( - num_edges_axis[0], num_edges_axis[0] + num_edges_axis[1] + num_faces_axis[0], num_faces_axis[0] + num_faces_axis[1] ) # top face connectivity_cell_to_horizontal_face[ np.ravel(numbering_cells[1:, :]), 1 ] = np.arange( - num_edges_axis[0], num_edges_axis[0] + num_edges_axis[1] + num_faces_axis[0], num_faces_axis[0] + num_faces_axis[1] ) # bottom face # Info about inner cells @@ -167,10 +167,10 @@ def _setup(self) -> None: inner_cells_with_horizontal_faces ) - # Define true RT0 mass matrix on edges: flat_fluxes -> flat_fluxes - mass_matrix_edges_data = np.prod(self.voxel_size) * np.concatenate( + # Define true RT0 mass matrix on faces: flat_fluxes -> flat_fluxes + mass_matrix_faces_data = np.prod(self.voxel_size) * np.concatenate( ( - 2 / 3 * np.ones(num_edges, dtype=float), # all faces + 2 / 3 * np.ones(num_faces, dtype=float), # all faces 1 / 6 * np.ones( @@ -193,9 +193,9 @@ def _setup(self) -> None: ), # bottom faces ) ) - mass_matrix_edges_row = np.concatenate( + mass_matrix_faces_row = np.concatenate( ( - np.arange(num_edges, dtype=int), + np.arange(num_faces, dtype=int), connectivity_cell_to_vertical_face[ inner_cells_with_vertical_faces, 0 ], @@ -210,9 +210,9 @@ def _setup(self) -> None: ], ) ) - mass_matrix_edges_col = np.concatenate( + mass_matrix_faces_col = np.concatenate( ( - np.arange(num_edges, dtype=int), + np.arange(num_faces, dtype=int), connectivity_cell_to_vertical_face[ inner_cells_with_vertical_faces, 1 ], @@ -227,12 +227,12 @@ def _setup(self) -> None: ], ) ) - self.mass_matrix_edges = sps.csc_matrix( + self.mass_matrix_faces = sps.csc_matrix( ( - mass_matrix_edges_data, - (mass_matrix_edges_row, mass_matrix_edges_col), + mass_matrix_faces_data, + (mass_matrix_faces_row, mass_matrix_faces_col), ), - shape=(num_edges, num_edges), + shape=(num_faces, num_faces), ) # Utilities @@ -251,20 +251,20 @@ def _setup(self) -> None: # Define sparse embedding operator for fluxes into full discrete DOF space self.flux_embedding = sps.csc_matrix( ( - np.ones(num_edges, dtype=float), - (np.arange(num_edges), np.arange(num_edges)), + np.ones(num_faces, dtype=float), + (np.arange(num_faces), np.arange(num_faces)), ), - shape=(num_edges + num_cells + 1, num_edges), + shape=(num_faces + num_cells + 1, num_faces), ) # Cache - self.num_edges = num_edges + self.num_faces = num_faces self.num_cells = num_cells self.dim_cells = dim_cells self.numbering_cells = numbering_cells - self.num_edges_axis = num_edges_axis - self.vertical_edges_shape = vertical_edges_shape - self.horizontal_edges_shape = horizontal_edges_shape + self.num_faces_axis = num_faces_axis + self.vertical_faces_shape = vertical_faces_shape + self.horizontal_faces_shape = horizontal_faces_shape def _problem_specific_setup(self, mass_diff: np.ndarray) -> None: """Resetup of fixed discretization""" @@ -305,8 +305,8 @@ def split_solution( """ # Split the solution - flat_flux = solution[: self.num_edges] - flat_potential = solution[self.num_edges : self.num_edges + self.num_cells] + flat_flux = solution[: self.num_faces] + flat_potential = solution[self.num_faces : self.num_faces + self.num_cells] flat_lagrange_multiplier = solution[-1] return flat_flux, flat_potential, flat_lagrange_multiplier @@ -326,11 +326,11 @@ def cell_reconstruction(self, flat_flux: np.ndarray) -> np.ndarray: # TODO replace by sparse matrix multiplication # Reshape fluxes - use duality of faces and normals - horizontal_fluxes = flat_flux[: self.num_edges_axis[0]].reshape( - self.vertical_edges_shape + horizontal_fluxes = flat_flux[: self.num_faces_axis[0]].reshape( + self.vertical_faces_shape ) - vertical_fluxes = flat_flux[self.num_edges_axis[0] :].reshape( - self.horizontal_edges_shape + vertical_fluxes = flat_flux[self.num_faces_axis[0] :].reshape( + self.horizontal_faces_shape ) # Determine a cell-based Raviart-Thomas reconstruction of the fluxes @@ -620,7 +620,7 @@ def residual(self, rhs: np.ndarray, solution: np.ndarray) -> np.ndarray: return ( rhs - self.broken_darcy.dot(solution) - - self.flux_embedding.dot(self.mass_matrix_edges.dot(flat_flux_normed)) + - self.flux_embedding.dot(self.mass_matrix_faces.dot(flat_flux_normed)) ) def jacobian_lu(self, solution: np.ndarray) -> sps.linalg.splu: @@ -644,7 +644,7 @@ def jacobian_lu(self, solution: np.ndarray) -> sps.linalg.splu: [ [ sps.diags(np.maximum(self.L, 1.0 / flat_flux_norm), dtype=float) - * self.mass_matrix_edges, + * self.mass_matrix_faces, -self.div.T, None, ], @@ -681,7 +681,7 @@ def _solve(self, flat_mass_diff: np.ndarray) -> tuple[float, np.ndarray, dict]: # Define right hand side rhs = np.concatenate( [ - np.zeros(self.num_edges, dtype=float), + np.zeros(self.num_faces, dtype=float), self.mass_matrix_cells.dot(flat_mass_diff), np.zeros(1, dtype=float), ] @@ -734,8 +734,8 @@ def _solve(self, flat_mass_diff: np.ndarray) -> tuple[float, np.ndarray, dict]: # Application to full solution, or just the potential, lead to divergence, # while application to the flux, results in improved performance. if self.anderson is not None: - solution_i[: self.num_edges] = self.anderson( - solution_i[: self.num_edges], update_i[: self.num_edges], iter + solution_i[: self.num_faces] = self.anderson( + solution_i[: self.num_faces], update_i[: self.num_faces], iter ) # Update distance @@ -749,9 +749,9 @@ def _solve(self, flat_mass_diff: np.ndarray) -> tuple[float, np.ndarray, dict]: # - distance increment error = [ np.linalg.norm(residual_i, 2), - np.linalg.norm(residual_i[self.num_edges : -1], 2), + np.linalg.norm(residual_i[self.num_faces : -1], 2), np.linalg.norm(solution_i - old_solution_i, 2), - np.linalg.norm((solution_i - old_solution_i)[: self.num_edges], 2), + np.linalg.norm((solution_i - old_solution_i)[: self.num_faces], 2), abs(new_distance - old_distance), ] @@ -805,7 +805,7 @@ def _problem_specific_setup(self, mass_diff: np.ndarray) -> None: self.L = self.options.get("L", 1.0) l_scheme_mixed_darcy = sps.bmat( [ - [self.L * self.mass_matrix_edges, -self.div.T, None], + [self.L * self.mass_matrix_faces, -self.div.T, None], [self.div, None, -self.potential_constraint.T], [None, self.potential_constraint, None], ], @@ -821,7 +821,7 @@ def _solve(self, flat_mass_diff): rhs = np.concatenate( [ - np.zeros(self.num_edges, dtype=float), + np.zeros(self.num_faces, dtype=float), self.mass_matrix_cells.dot(flat_mass_diff), np.zeros(1, dtype=float), ] @@ -838,7 +838,7 @@ def _solve(self, flat_mass_diff): # 1. Solve linear system with trust in current flux. flat_flux_i, _, _ = self.split_solution(solution_i) rhs_i = rhs.copy() - rhs_i[: self.num_edges] = self.L * self.mass_matrix_edges.dot(flat_flux_i) + rhs_i[: self.num_faces] = self.L * self.mass_matrix_faces.dot(flat_flux_i) intermediate_solution_i = self.l_scheme_mixed_darcy_lu.solve(rhs_i) # 2. Shrink step for vectorial fluxes. To comply with the RT0 setting, the @@ -857,7 +857,7 @@ def _solve(self, flat_mass_diff): cell_scaling = np.maximum(norm - 1 / self.L, 0) / ( norm + self.regularization ) - flat_scaling = self.face_restriction_scalar(cell_scaling) + flat_scaling = self.face_restriction_scalar(cell_scaling, mode="arithmetic") new_flat_flux_i = flat_scaling * intermediate_flat_flux_i # Apply Anderson acceleration to flux contribution (the only nonlinear part). @@ -870,14 +870,14 @@ def _solve(self, flat_mass_diff): # Update flux solution solution_i = intermediate_solution_i.copy() - solution_i[: self.num_edges] = new_flat_flux_i + solution_i[: self.num_faces] = new_flat_flux_i # Update distance new_distance = self.l1_dissipation(solution_i) # Determine the error in the mass conservation equation mass_conservation_residual = np.linalg.norm( - (rhs_i - self.broken_darcy.dot(solution_i))[self.num_edges : -1], 2 + (rhs_i - self.broken_darcy.dot(solution_i))[self.num_faces : -1], 2 ) # TODO include criterion build on staganation of the solution @@ -920,7 +920,7 @@ def _solve(self, flat_mass_diff): # Update linear system l_scheme_mixed_darcy = sps.bmat( [ - [self.L * self.mass_matrix_edges, -self.div.T, None], + [self.L * self.mass_matrix_faces, -self.div.T, None], [self.div, None, -self.potential_constraint.T], [None, self.potential_constraint, None], ], From adb851427c058ef94e889556b249df74cbeac550 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Mon, 28 Aug 2023 22:14:04 +0200 Subject: [PATCH 012/100] DOC: Improve documentation of connectivity infrastructure. --- src/darsia/measure/wasserstein.py | 44 +++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index 9873055c..fa0f2db3 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -66,18 +66,19 @@ def __init__( self.regularization = self.options.get("regularization", 0.0) self.verbose = self.options.get("verbose", False) - # Setup + # Setup of finite volume discretization self._setup() def _setup(self) -> None: """Setup of fixed discretization""" - # Define dimensions of the problem + # Define dimensions of the problem and indexing of cells dim_cells = self.shape num_cells = np.prod(dim_cells) numbering_cells = np.arange(num_cells, dtype=int).reshape(dim_cells) - # Consider only inner faces + # Consider only inner faces; implicitly define indexing of faces (first + # vertical, then horizontal) vertical_faces_shape = (self.shape[0], self.shape[1] - 1) horizontal_faces_shape = (self.shape[0] - 1, self.shape[1]) num_faces_axis = [ @@ -90,18 +91,24 @@ def _setup(self) -> None: connectivity = np.zeros((num_faces, 2), dtype=int) connectivity[: num_faces_axis[0], 0] = np.ravel( numbering_cells[:, :-1] - ) # left cells + ) # vertical faces, cells to the left connectivity[: num_faces_axis[0], 1] = np.ravel( numbering_cells[:, 1:] - ) # right cells + ) # vertical faces, cells to the right connectivity[num_faces_axis[0] :, 0] = np.ravel( numbering_cells[:-1, :] - ) # top cells + ) # horizontal faces, cells to the top connectivity[num_faces_axis[0] :, 1] = np.ravel( numbering_cells[1:, :] - ) # bottom cells - - # Define sparse divergence operator, integrated over elements: flat_fluxes -> flat_mass + ) # horizontal faces, cells to the bottom + + # Define sparse divergence operator, integrated over elements. + # Note: The global direction of the degrees of freedom is hereby fixed for all + # faces. Fluxes across vertical faces go from left to right, fluxes across + # horizontal faces go from bottom to top. To oppose the direction of the outer + # normal, the sign of the divergence is flipped for one side of cells for all + # faces. + div_shape = (num_cells, num_faces) div_data = np.concatenate( ( self.voxel_size[0] * np.ones(num_faces_axis[0], dtype=float), @@ -112,15 +119,24 @@ def _setup(self) -> None: ) div_row = np.concatenate( ( - connectivity[: num_faces_axis[0], 0], - connectivity[num_faces_axis[0] :, 0], - connectivity[: num_faces_axis[0], 1], - connectivity[num_faces_axis[0] :, 1], + connectivity[ + : num_faces_axis[0], 0 + ], # vertical faces, cells to the left + connectivity[ + num_faces_axis[0] :, 0 + ], # horizontal faces, cells to the top + connectivity[ + : num_faces_axis[0], 1 + ], # vertical faces, cells to the right (opposite normal) + connectivity[ + num_faces_axis[0] :, 1 + ], # horizontal faces, cells to the bottom (opposite normal) ) ) div_col = np.tile(np.arange(num_faces, dtype=int), 2) self.div = sps.csc_matrix( - (div_data, (div_row, div_col)), shape=(num_cells, num_faces) + (div_data, (div_row, div_col)), + shape=div_shape, ) # Define sparse mass matrix on cells: flat_mass -> flat_mass From 80b255ba962fbce45dd79e00ff6fbdbc33dab4fa Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Tue, 29 Aug 2023 13:25:12 +0200 Subject: [PATCH 013/100] ENH: Add more grid operators for face restictions. --- src/darsia/measure/wasserstein.py | 259 ++++++++++++++++++++++-------- 1 file changed, 190 insertions(+), 69 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index fa0f2db3..96c2b61d 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -72,35 +72,88 @@ def __init__( def _setup(self) -> None: """Setup of fixed discretization""" - # Define dimensions of the problem and indexing of cells + # ! ---- Grid management ---- + + # Define dimensions of the problem and indexing of cells, from here one start + # counting rows from left to right, from top to bottom. dim_cells = self.shape num_cells = np.prod(dim_cells) - numbering_cells = np.arange(num_cells, dtype=int).reshape(dim_cells) + flat_numbering_cells = np.arange(num_cells, dtype=int) + numbering_cells = flat_numbering_cells.reshape(dim_cells) # Consider only inner faces; implicitly define indexing of faces (first - # vertical, then horizontal) + # vertical, then horizontal). The counting of vertical faces starts from top to + # bottom and left to right. The counting of horizontal faces starts from left to + # right and top to bottom. vertical_faces_shape = (self.shape[0], self.shape[1] - 1) horizontal_faces_shape = (self.shape[0] - 1, self.shape[1]) + num_vertical_faces = np.prod(vertical_faces_shape) + num_horizontal_faces = np.prod(horizontal_faces_shape) num_faces_axis = [ - np.prod(vertical_faces_shape), - np.prod(horizontal_faces_shape), + num_vertical_faces, + num_horizontal_faces, ] num_faces = np.sum(num_faces_axis) + # Define flat indexing of faces: vertical faces first, then horizontal faces + flat_vertical_faces = np.arange(num_vertical_faces, dtype=int) + flat_horizontal_faces = num_vertical_faces + np.arange( + num_horizontal_faces, dtype=int + ) + vertical_faces = flat_vertical_faces.reshape(vertical_faces_shape) + horizontal_faces = flat_horizontal_faces.reshape(horizontal_faces_shape) + + # Identify vertical faces on top, inner and bottom + top_row_vertical_faces = np.ravel(vertical_faces[0, :]) + inner_vertical_faces = np.ravel(vertical_faces[1:-1, :]) + bottom_row_vertical_faces = np.ravel(vertical_faces[-1, :]) + # Identify horizontal faces on left, inner and right + left_col_horizontal_faces = np.ravel(horizontal_faces[:, 0]) + inner_horizontal_faces = np.ravel(horizontal_faces[:, 1:-1]) + right_col_horizontal_faces = np.ravel(horizontal_faces[:, -1]) + + # ! ---- Connectivity ---- + # Define connectivity and direction of the normal on faces connectivity = np.zeros((num_faces, 2), dtype=int) - connectivity[: num_faces_axis[0], 0] = np.ravel( - numbering_cells[:, :-1] - ) # vertical faces, cells to the left - connectivity[: num_faces_axis[0], 1] = np.ravel( - numbering_cells[:, 1:] - ) # vertical faces, cells to the right - connectivity[num_faces_axis[0] :, 0] = np.ravel( - numbering_cells[:-1, :] - ) # horizontal faces, cells to the top - connectivity[num_faces_axis[0] :, 1] = np.ravel( - numbering_cells[1:, :] - ) # horizontal faces, cells to the bottom + # Vertical faces to left cells + connectivity[: num_faces_axis[0], 0] = np.ravel(numbering_cells[:, :-1]) + # Vertical faces to right cells + connectivity[: num_faces_axis[0], 1] = np.ravel(numbering_cells[:, 1:]) + # Horizontal faces to top cells + connectivity[num_faces_axis[0] :, 0] = np.ravel(numbering_cells[:-1, :]) + # Horizontal faces to bottom cells + connectivity[num_faces_axis[0] :, 1] = np.ravel(numbering_cells[1:, :]) + + # Define reverse connectivity. Cell to vertical faces + connectivity_cell_to_vertical_face = -np.ones((num_cells, 2), dtype=int) + # Left vertical face of cell + connectivity_cell_to_vertical_face[ + np.ravel(numbering_cells[:, 1:]), 0 + ] = flat_vertical_faces + # Right vertical face of cell + connectivity_cell_to_vertical_face[ + np.ravel(numbering_cells[:, :-1]), 1 + ] = flat_vertical_faces + # Define reverse connectivity. Cell to horizontal faces + connectivity_cell_to_horizontal_face = np.zeros((num_cells, 2), dtype=int) + # Top horizontal face of cell + connectivity_cell_to_horizontal_face[ + np.ravel(numbering_cells[1:, :]), 0 + ] = flat_horizontal_faces + # Bottom horizontal face of cell + connectivity_cell_to_horizontal_face[ + np.ravel(numbering_cells[:-1, :]), 1 + ] = flat_horizontal_faces + + # Info about inner cells + # TODO rm? + inner_cells_with_vertical_faces = np.ravel(numbering_cells[:, 1:-1]) + inner_cells_with_horizontal_faces = np.ravel(numbering_cells[1:-1, :]) + num_inner_cells_with_vertical_faces = len(inner_cells_with_vertical_faces) + num_inner_cells_with_horizontal_faces = len(inner_cells_with_horizontal_faces) + + # ! ---- Operators ---- # Define sparse divergence operator, integrated over elements. # Note: The global direction of the degrees of freedom is hereby fixed for all @@ -111,25 +164,25 @@ def _setup(self) -> None: div_shape = (num_cells, num_faces) div_data = np.concatenate( ( - self.voxel_size[0] * np.ones(num_faces_axis[0], dtype=float), - self.voxel_size[1] * np.ones(num_faces_axis[1], dtype=float), - -self.voxel_size[0] * np.ones(num_faces_axis[0], dtype=float), - -self.voxel_size[1] * np.ones(num_faces_axis[1], dtype=float), + self.voxel_size[0] * np.ones(num_vertical_faces, dtype=float), + self.voxel_size[1] * np.ones(num_horizontal_faces, dtype=float), + -self.voxel_size[0] * np.ones(num_vertical_faces, dtype=float), + -self.voxel_size[1] * np.ones(num_horizontal_faces, dtype=float), ) ) div_row = np.concatenate( ( connectivity[ - : num_faces_axis[0], 0 + flat_vertical_faces, 0 ], # vertical faces, cells to the left connectivity[ - num_faces_axis[0] :, 0 + flat_horizontal_faces, 0 ], # horizontal faces, cells to the top connectivity[ - : num_faces_axis[0], 1 + flat_vertical_faces, 1 ], # vertical faces, cells to the right (opposite normal) connectivity[ - num_faces_axis[0] :, 1 + flat_horizontal_faces, 1 ], # horizontal faces, cells to the bottom (opposite normal) ) ) @@ -151,39 +204,8 @@ def _setup(self) -> None: np.prod(self.voxel_size) * np.ones(num_faces, dtype=float) ) else: - # Define connectivity: cell to face (only for inner cells) - connectivity_cell_to_vertical_face = np.zeros((num_cells, 2), dtype=int) - connectivity_cell_to_vertical_face[ - np.ravel(numbering_cells[:, :-1]), 0 - ] = np.arange( - num_faces_axis[0] - ) # left face - connectivity_cell_to_vertical_face[ - np.ravel(numbering_cells[:, 1:]), 1 - ] = np.arange( - num_faces_axis[0] - ) # right face - connectivity_cell_to_horizontal_face = np.zeros((num_cells, 2), dtype=int) - connectivity_cell_to_horizontal_face[ - np.ravel(numbering_cells[:-1, :]), 0 - ] = np.arange( - num_faces_axis[0], num_faces_axis[0] + num_faces_axis[1] - ) # top face - connectivity_cell_to_horizontal_face[ - np.ravel(numbering_cells[1:, :]), 1 - ] = np.arange( - num_faces_axis[0], num_faces_axis[0] + num_faces_axis[1] - ) # bottom face - - # Info about inner cells - inner_cells_with_vertical_faces = np.ravel(numbering_cells[:, 1:-1]) - inner_cells_with_horizontal_faces = np.ravel(numbering_cells[1:-1, :]) - num_inner_cells_with_vertical_faces = len(inner_cells_with_vertical_faces) - num_inner_cells_with_horizontal_faces = len( - inner_cells_with_horizontal_faces - ) - # Define true RT0 mass matrix on faces: flat_fluxes -> flat_fluxes + mass_matrix_faces_shape = (num_faces, num_faces) mass_matrix_faces_data = np.prod(self.voxel_size) * np.concatenate( ( 2 / 3 * np.ones(num_faces, dtype=float), # all faces @@ -248,21 +270,109 @@ def _setup(self) -> None: mass_matrix_faces_data, (mass_matrix_faces_row, mass_matrix_faces_col), ), - shape=(num_faces, num_faces), + shape=mass_matrix_faces_shape, ) - # Utilities - aa_depth = self.options.get("aa_depth", 0) - aa_restart = self.options.get("aa_restart", None) - self.anderson = ( - darsia.AndersonAcceleration( - dimension=None, depth=aa_depth, restart=aa_restart + # Operator for averaging fluxes on orthogonal, neighboring faces + orthogonal_face_average_shape = (num_faces, num_faces) + orthogonal_face_average_data = 0.25 * np.concatenate( + ( + np.ones( + 2 * len(top_row_vertical_faces) + + 4 * len(inner_vertical_faces) + + 2 * len(bottom_row_vertical_faces) + + 2 * len(left_col_horizontal_faces) + + 4 * len(inner_horizontal_faces) + + 2 * len(right_col_horizontal_faces), + dtype=float, + ), ) - if aa_depth > 0 - else None ) - - # TODO needs to be defined for each problem separately + orthogonal_face_average_rows = np.concatenate( + ( + np.tile(top_row_vertical_faces, 2), + np.tile(inner_vertical_faces, 4), + np.tile(bottom_row_vertical_faces, 2), + np.tile(left_col_horizontal_faces, 2), + np.tile(inner_horizontal_faces, 4), + np.tile(right_col_horizontal_faces, 2), + ) + ) + orthogonal_face_average_cols = np.concatenate( + ( + # top row: left cell -> bottom face + connectivity_cell_to_horizontal_face[ + connectivity[top_row_vertical_faces, 0], 1 + ], + # top row: vertical face -> right cell -> bottom face + connectivity_cell_to_horizontal_face[ + connectivity[top_row_vertical_faces, 1], 1 + ], + # inner rows: vertical face -> left cell -> top face + connectivity_cell_to_horizontal_face[ + connectivity[inner_vertical_faces, 0], 0 + ], + # inner rows: vertical face -> left cell -> bottom face + connectivity_cell_to_horizontal_face[ + connectivity[inner_vertical_faces, 0], 1 + ], + # inner rows: vertical face -> right cell -> top face + connectivity_cell_to_horizontal_face[ + connectivity[inner_vertical_faces, 1], 0 + ], + # inner rows: vertical face -> right cell -> bottom face + connectivity_cell_to_horizontal_face[ + connectivity[inner_vertical_faces, 1], 1 + ], + # bottom row: vertical face -> left cell -> top face + connectivity_cell_to_horizontal_face[ + connectivity[bottom_row_vertical_faces, 0], 0 + ], + # bottom row: vertical face -> right cell -> top face + connectivity_cell_to_horizontal_face[ + connectivity[bottom_row_vertical_faces, 1], 0 + ], + # left column: horizontal face -> top cell -> right face + connectivity_cell_to_vertical_face[ + connectivity[left_col_horizontal_faces, 0], 1 + ], + # left column: horizontal face -> bottom cell -> right face + connectivity_cell_to_vertical_face[ + connectivity[left_col_horizontal_faces, 1], 1 + ], + # inner columns: horizontal face -> top cell -> left face + connectivity_cell_to_vertical_face[ + connectivity[inner_horizontal_faces, 0], 0 + ], + # inner columns: horizontal face -> top cell -> right face + connectivity_cell_to_vertical_face[ + connectivity[inner_horizontal_faces, 0], 1 + ], + # inner columns: horizontal face -> bottom cell -> left face + connectivity_cell_to_vertical_face[ + connectivity[inner_horizontal_faces, 1], 0 + ], + # inner columns: horizontal face -> bottom cell -> right face + connectivity_cell_to_vertical_face[ + connectivity[inner_horizontal_faces, 1], 1 + ], + # right column: horizontal face -> top cell -> left face + connectivity_cell_to_vertical_face[ + connectivity[right_col_horizontal_faces, 0], 0 + ], + # right column: horizontal face -> bottom cell -> left face + connectivity_cell_to_vertical_face[ + connectivity[right_col_horizontal_faces, 1], 0 + ], + ) + ) + self.orthogonal_face_average = sps.csc_matrix( + ( + orthogonal_face_average_data, + (orthogonal_face_average_rows, orthogonal_face_average_cols), + ), + shape=orthogonal_face_average_shape, + ) # Define sparse embedding operator for fluxes into full discrete DOF space self.flux_embedding = sps.csc_matrix( @@ -273,7 +383,18 @@ def _setup(self) -> None: shape=(num_faces + num_cells + 1, num_faces), ) - # Cache + # ! ---- Utilities ---- + aa_depth = self.options.get("aa_depth", 0) + aa_restart = self.options.get("aa_restart", None) + self.anderson = ( + darsia.AndersonAcceleration( + dimension=None, depth=aa_depth, restart=aa_restart + ) + if aa_depth > 0 + else None + ) + + # ! ---- Cache ---- self.num_faces = num_faces self.num_cells = num_cells self.dim_cells = dim_cells From 3efb8a64e5453182d88ed8e67f637b635490aeaa Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Tue, 29 Aug 2023 13:25:49 +0200 Subject: [PATCH 014/100] MAINT: Move face-flux computations part of the common Wasserstein class. --- src/darsia/measure/wasserstein.py | 72 ++++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 11 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index 96c2b61d..d6b20f52 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -554,6 +554,50 @@ def l1_dissipation(self, solution: np.ndarray) -> float: cell_flux = self.cell_reconstruction(flat_flux) return np.sum(np.prod(self.voxel_size) * np.linalg.norm(cell_flux, 2, axis=-1)) + # ! ---- Lumping of effective mobility + + def face_flux_norm(self, solution: np.ndarray, mode: str) -> np.ndarray: + """Compute the norm of the fluxes on the faces. + + Args: + solution (np.ndarray): solution + mode (str): mode of the norm + + Returns: + np.ndarray: norm of the fluxes on the faces + + """ + + # Extract the fluxes + flat_flux, _, _ = self.split_solution(solution) + + # Determine the norm of the fluxes on the faces + if mode in ["cell_arithmetic", "cell_harmonic"]: + # Consider the piecewise constant projection of vector valued fluxes + cell_flux = self.cell_reconstruction(flat_flux) + # Determine the norm of the fluxes on the cells + cell_flux_norm = np.maximum( + np.linalg.norm(cell_flux, 2, axis=-1), self.regularization + ) + # Take the average over faces + if mode == "cell_arithmetic": + flat_flux_norm = self.face_restriction_scalar( + cell_flux_norm, mode="arithmetic" + ) + elif mode == "cell_harmonic": + flat_flux_norm = self.face_restriction_scalar( + cell_flux_norm, mode="harmonic" + ) + elif mode == "face_arithmetic": + # Define natural vector valued flux on faces (taking averages over cells) + tangential_flux = self.orthogonal_face_average.dot(flat_flux) + # Determine the l2 norm of the fluxes on the faces, add some regularization + flat_flux_norm = np.sqrt(flat_flux**2 + tangential_flux**2) + else: + raise ValueError(f"Mode {mode} not supported.") + + return flat_flux_norm + # ! ---- Main methods ---- def __call__( @@ -748,12 +792,20 @@ def residual(self, rhs: np.ndarray, solution: np.ndarray) -> np.ndarray: """ flat_flux, _, _ = self.split_solution(solution) - cell_flux = self.cell_reconstruction(flat_flux) - cell_flux_norm = np.maximum( - np.linalg.norm(cell_flux, 2, axis=-1), self.regularization + # TODO cell-based version meaningful? + # cell_flux = self.cell_reconstruction(flat_flux) + # cell_flux_norm = np.maximum( + # np.linalg.norm(cell_flux, 2, axis=-1), self.regularization + # ) + # cell_flux_normed = cell_flux / cell_flux_norm[..., None] + # flat_flux_normed = self.face_restriction(cell_flux_normed) + mode = "face_arithmetic" + #mode = "cell_arithmetic" + flat_flux_norm = np.maximum( + self.face_flux_norm(solution, mode=mode), self.regularization ) - cell_flux_normed = cell_flux / cell_flux_norm[..., None] - flat_flux_normed = self.face_restriction(cell_flux_normed) + flat_flux_normed = flat_flux / flat_flux_norm + return ( rhs - self.broken_darcy.dot(solution) @@ -770,13 +822,11 @@ def jacobian_lu(self, solution: np.ndarray) -> sps.linalg.splu: sps.linalg.splu: LU factorization of the jacobian """ - flat_flux, _, _ = self.split_solution(solution) - cell_flux = self.cell_reconstruction(flat_flux) - self.regularization = self.options.get("regularization", 0.0) - cell_flux_norm = np.maximum( - np.linalg.norm(cell_flux, 2, axis=-1), self.regularization + mode = "face_arithmetic" + #mode = "cell_arithmetic" + flat_flux_norm = np.maximum( + self.face_flux_norm(solution, mode=mode), self.regularization ) - flat_flux_norm = self.face_restriction_scalar(cell_flux_norm) approx_jacobian = sps.bmat( [ [ From 6e5e8278a8cbc7c675b6f0265e108bac4257ff3f Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Wed, 30 Aug 2023 21:21:28 +0200 Subject: [PATCH 015/100] ENH: Revise solver structure for Newton steps and add more monitoring. --- src/darsia/measure/wasserstein.py | 219 ++++++++++++++++++++++++++---- 1 file changed, 196 insertions(+), 23 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index d6b20f52..fa8c9456 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -8,6 +8,7 @@ import matplotlib.pyplot as plt import numpy as np +import pyamg import scipy.sparse as sps import darsia @@ -812,7 +813,7 @@ def residual(self, rhs: np.ndarray, solution: np.ndarray) -> np.ndarray: - self.flux_embedding.dot(self.mass_matrix_faces.dot(flat_flux_normed)) ) - def jacobian_lu(self, solution: np.ndarray) -> sps.linalg.splu: + def jacobian(self, solution: np.ndarray) -> sps.linalg.LinearOperator: """Compute the LU factorization of the jacobian of the solution. Args: @@ -823,7 +824,7 @@ def jacobian_lu(self, solution: np.ndarray) -> sps.linalg.splu: """ mode = "face_arithmetic" - #mode = "cell_arithmetic" + # mode = "cell_arithmetic" flat_flux_norm = np.maximum( self.face_flux_norm(solution, mode=mode), self.regularization ) @@ -840,8 +841,158 @@ def jacobian_lu(self, solution: np.ndarray) -> sps.linalg.splu: ], format="csc", ) - approx_jacobian_lu = sps.linalg.splu(approx_jacobian) - return approx_jacobian_lu + return approx_jacobian + + def darcy_jacobian(self) -> sps.linalg.LinearOperator: + """Compute the LU factorization of the jacobian of the solution.""" + L_init = self.options.get("L_init", 1.0) + jacobian = sps.bmat( + [ + [L_init * self.mass_matrix_faces, -self.div.T, None], + [self.div, None, -self.potential_constraint.T], + [None, self.potential_constraint, None], + ], + format="csc", + ) + return jacobian + + def setup_infrastructure(self) -> None: + """Setup the infrastructure for reduced systems through Gauss elimination. + + Provide internal data structures for the reduced system. + + """ + + # TODO build the sparsity pattern explicitly + + # The Darcy problem is sufficient + jacobian = self.darcy_jacobian() + + # Build Schur complement wrt. flux-flux block + J = jacobian[: self.num_faces, : self.num_faces] + J_inv = sps.diags(1.0 / J.diagonal()) + D = jacobian[self.num_faces :, : self.num_faces] + schur_complement = D.dot(J_inv.dot(D.T)) + + # Add Schur complement - use this to identify sparsity structure + # Cache the reduced jacobian + self.reduced_jacobian = ( + jacobian[self.num_faces :, self.num_faces :] + schur_complement + ) + + def linearization_step( + self, solution: np.ndarray, rhs: np.ndarray, iter: int + ) -> tuple[np.ndarray, np.ndarray, list[float]]: + """Newton step for the linearization of the problem. + + In the first iteration, the linearization is the linearization of the Darcy + problem. + + Args: + solution (np.ndarray): solution + rhs (np.ndarray): right hand side + iter (int): iteration number + + Returns: + tuple: update, residual, stats (timinings) + + """ + tic = time.time() + # Determine residual + if iter == 0: + residual = rhs.copy() + else: + residual = self.residual(rhs, solution) + + # Setup linear solver + linear_solver = self.options.get("linear_solver", "lu") + if iter == 0: + approx_jacobian = self.darcy_jacobian() + else: + approx_jacobian = self.jacobian(solution) + + toc = time.time() + time_setup = toc - tic + tic = time.time() + + # Solve linear system for the update + if linear_solver == "lu": + jacobian_lu = sps.linalg.splu(approx_jacobian) + update = jacobian_lu.solve(residual) + elif linear_solver == "amg": + # Build Schur complement wrt flux-block + J = approx_jacobian[: self.num_faces, : self.num_faces] + J_inv = sps.diags(1.0 / J.diagonal()) + D = approx_jacobian[self.num_faces :, : self.num_faces] + schur_complement = D.dot(J_inv.dot(D.T)) + + # Gauss eliminiation on matrices + self.reduced_jacobian.data[:] = 0.0 + self.reduced_jacobian += approx_jacobian[self.num_faces :, self.num_faces :] + self.reduced_jacobian += schur_complement + + # Gauss elimination on vectors + reduced_residual = residual[self.num_faces :] + reduced_residual -= D.dot(J_inv.dot(residual[: self.num_faces])) + + # TODO reduce to pure pressure system! + + # ML architecture + ml = pyamg.smoothed_aggregation_solver( + self.reduced_jacobian, + # B=X.reshape( + # n * n, 1 + # ), # the representation of the near null space (this is a poor choice) + # BH=None, # the representation of the left near null space + symmetry="hermitian", # indicate that the matrix is Hermitian + # strength="evolution", # change the strength of connection + aggregate="standard", # use a standard aggregation method + smooth=( + "jacobi", + {"omega": 4.0 / 3.0, "degree": 2}, + ), # prolongation smoothing + presmoother=("block_gauss_seidel", {"sweep": "symmetric"}), + postsmoother=("block_gauss_seidel", {"sweep": "symmetric"}), + # improve_candidates=[ + # ("block_gauss_seidel", {"sweep": "symmetric", "iterations": 4}), + # None, + # ], + max_levels=4, # maximum number of levels + max_coarse=1000, # maximum number on a coarse level + # keep=False, # keep extra operators around in the hierarchy (memory) + ) + if False: + print(ml) + + tol_linear_solver = self.options.get("tol_linear_solver", 1e-6) + res_history = [] + update = np.zeros_like(solution, dtype=float) + update[self.num_faces :] = ml.solve( + reduced_residual, tol=tol_linear_solver, residuals=res_history + ) + # TODO rm only for debugging + # lu = sps.linalg.splu(self.reduced_jacobian) + # update[self.num_faces :] = lu.solve(reduced_residual) + if False: + print( + "res: ", + len(res_history), + np.linalg.norm( + reduced_residual + - self.reduced_jacobian.dot(update[self.num_faces :]) + ), + ) + print("update", np.linalg.norm(update)) + + # Compute flux update + update[: self.num_faces] = J_inv.dot( + residual[: self.num_faces] + D.T.dot(update[self.num_faces :]) + ) + toc = time.time() + time_solve = toc - tic + stats = [time_setup, time_solve] + + return update, residual, stats def _solve(self, flat_mass_diff: np.ndarray) -> tuple[float, np.ndarray, dict]: """Solve the Beckman problem using Newton's method. @@ -853,8 +1004,15 @@ def _solve(self, flat_mass_diff: np.ndarray) -> tuple[float, np.ndarray, dict]: tuple: distance, solution, status """ - # TODO rm: Observation: AA can lead to less stagnation, more accurate results, and therefore - # better solutions to mu and u. Higher depth is better, but more expensive. + # TODO rm: Observation: AA can lead to less stagnation, more accurate results, + # and therefore better solutions to mu and u. Higher depth is better, but more + # expensive. + + # Setup + tic = time.time() + self.setup_infrastructure() + time_infrastructure = time.time() - tic + print("timing infra structure", time_infrastructure) # Solver parameters num_iter = self.options.get("num_iter", 100) @@ -881,10 +1039,11 @@ def _solve(self, flat_mass_diff: np.ndarray) -> tuple[float, np.ndarray, dict]: convergence_history = { "distance": [], "residual": [], - "mass conservation residual": [], + "decomposed residual": [], "increment": [], - "flux increment": [], + "decomposed increment": [], "distance increment": [], + "timing": [], } # Print header @@ -907,48 +1066,61 @@ def _solve(self, flat_mass_diff: np.ndarray) -> tuple[float, np.ndarray, dict]: old_distance = self.l1_dissipation(solution_i) # Newton step - if iter == 0: - residual_i = ( - rhs.copy() - ) # Aim at Darcy-like initial guess after first iteration. - else: - residual_i = self.residual(rhs, solution_i) - jacobian_lu = self.jacobian_lu(solution_i) - update_i = jacobian_lu.solve(residual_i) + update_i, residual_i, stats_i = self.linearization_step( + solution_i, rhs, iter + ) solution_i += update_i # Apply Anderson acceleration to flux contribution (the only nonlinear part). # Application to full solution, or just the potential, lead to divergence, # while application to the flux, results in improved performance. + tic = time.time() if self.anderson is not None: solution_i[: self.num_faces] = self.anderson( solution_i[: self.num_faces], update_i[: self.num_faces], iter ) + toc = time.time() + time_anderson = toc - tic + stats_i.append(time_anderson) # Update distance new_distance = self.l1_dissipation(solution_i) # Compute the error: - # - residual + # - full residual + # - residual of the flux equation # - residual of mass conservation equation - # - increment + # - residual of the constraint equation + # - full increment # - flux increment + # - pressure increment + # - lagrange multiplier increment # - distance increment + increment = solution_i - old_solution_i error = [ np.linalg.norm(residual_i, 2), - np.linalg.norm(residual_i[self.num_faces : -1], 2), - np.linalg.norm(solution_i - old_solution_i, 2), - np.linalg.norm((solution_i - old_solution_i)[: self.num_faces], 2), + [ + np.linalg.norm(residual_i[: self.num_faces], 2), + np.linalg.norm(residual_i[self.num_faces : -1], 2), + np.linalg.norm(residual_i[-1:], 2), + ], + np.linalg.norm(increment, 2), + [ + np.linalg.norm(increment[: self.num_faces], 2), + np.linalg.norm(increment[self.num_faces : -1], 2), + np.linalg.norm(increment[-1:], 2), + ], abs(new_distance - old_distance), ] # Update convergence history convergence_history["distance"].append(new_distance) convergence_history["residual"].append(error[0]) - convergence_history["mass conservation residual"].append(error[1]) + convergence_history["decomposed residual"].append(error[1]) convergence_history["increment"].append(error[2]) - convergence_history["flux increment"].append(error[3]) + convergence_history["decomposed increment"].append(error[3]) convergence_history["distance increment"].append(error[4]) + convergence_history["timing"].append(stats_i) if self.verbose: print( @@ -960,6 +1132,7 @@ def _solve(self, flat_mass_diff: np.ndarray) -> tuple[float, np.ndarray, dict]: error[2], # full increment error[3], # flux increment error[4], # distance increment + stats_i, # timing ) # Stopping criterion From 721bc59d1223bcaced2afc781d949103b1932857 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Wed, 30 Aug 2023 21:45:00 +0200 Subject: [PATCH 016/100] MAINT: Switch to fixing zero potential at the center of the grid. --- src/darsia/measure/wasserstein.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index fa8c9456..f8807e6c 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -82,6 +82,10 @@ def _setup(self) -> None: flat_numbering_cells = np.arange(num_cells, dtype=int) numbering_cells = flat_numbering_cells.reshape(dim_cells) + # Define center cell + center_cell = np.array([self.shape[0] // 2, self.shape[1] // 2]).astype(int) + self.flat_center_cell = np.ravel_multi_index(center_cell, dim_cells) + # Consider only inner faces; implicitly define indexing of faces (first # vertical, then horizontal). The counting of vertical faces starts from top to # bottom and left to right. The counting of horizontal faces starts from left to @@ -407,10 +411,8 @@ def _setup(self) -> None: def _problem_specific_setup(self, mass_diff: np.ndarray) -> None: """Resetup of fixed discretization""" - # TODO can't we just fix some cell, e.g., [0,0]. Move then this to the above. - - # Fix index of dominating contribution in image differece - self.constrained_cell_flat_index = np.argmax(np.abs(mass_diff)) + # Fix index of center cell + self.constrained_cell_flat_index = self.flat_center_cell self.potential_constraint = sps.csc_matrix( ( np.ones(1, dtype=float), From dcb78b558c82aa78270b09e5529cad9bd0d947b2 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Sat, 2 Sep 2023 15:05:12 +0200 Subject: [PATCH 017/100] ENH: Add possibility to utilize true potential system through gauss --- src/darsia/measure/wasserstein.py | 357 ++++++++++++++++++++++++------ 1 file changed, 292 insertions(+), 65 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index f8807e6c..f941a084 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -864,23 +864,185 @@ def setup_infrastructure(self) -> None: Provide internal data structures for the reduced system. """ - - # TODO build the sparsity pattern explicitly + # Step 1: Compute the jacobian of the Darcy problem # The Darcy problem is sufficient jacobian = self.darcy_jacobian() + # Step 2: Remove flux blocks through Schur complement approach + # Build Schur complement wrt. flux-flux block - J = jacobian[: self.num_faces, : self.num_faces] + J = jacobian[: self.num_faces, : self.num_faces].copy() J_inv = sps.diags(1.0 / J.diagonal()) - D = jacobian[self.num_faces :, : self.num_faces] + D = jacobian[self.num_faces :, : self.num_faces].copy() schur_complement = D.dot(J_inv.dot(D.T)) + # Cache divergence matrix + self.D = D.copy() + self.DT = self.D.T.copy() + + # Cache (constant) jacobian subblock + self.jacobian_subblock = jacobian[self.num_faces :, self.num_faces :].copy() + # Add Schur complement - use this to identify sparsity structure # Cache the reduced jacobian - self.reduced_jacobian = ( - jacobian[self.num_faces :, self.num_faces :] + schur_complement + self.reduced_jacobian = self.jacobian_subblock + schur_complement + + # Step 3: Remove potential block through Gauss elimination + + # Find row entries to be removed + rm_row_entries = np.arange( + self.reduced_jacobian.indptr[self.constrained_cell_flat_index], + self.reduced_jacobian.indptr[self.constrained_cell_flat_index + 1], + ) + + # Find column entries to be removed + rm_col_entries = np.where( + self.reduced_jacobian.indices == self.constrained_cell_flat_index + )[0] + + # Collect all entries to be removes + rm_indices = np.unique( + np.concatenate((rm_row_entries, rm_col_entries)).astype(int) + ) + # Cache for later use in remove_lagrange_multiplier + self.rm_indices = rm_indices + + # Identify rows to be reduced + rm_rows = [ + np.max(np.where(self.reduced_jacobian.indptr <= index)[0]) + for index in rm_indices + ] + + # Reduce data - simply remove + fully_reduced_jacobian_data = np.delete(self.reduced_jacobian.data, rm_indices) + + # Reduce indices - remove and shift + fully_reduced_jacobian_indices = np.delete( + self.reduced_jacobian.indices, rm_indices + ) + fully_reduced_jacobian_indices[ + fully_reduced_jacobian_indices > self.constrained_cell_flat_index + ] -= 1 + + # Reduce indptr - shift and remove + # NOTE: As only a few entries should be removed, this is not too expensive + # and a for loop is used + fully_reduced_jacobian_indptr = self.reduced_jacobian.indptr.copy() + for row in rm_rows: + fully_reduced_jacobian_indptr[row + 1 :] -= 1 + fully_reduced_jacobian_indptr = np.unique(fully_reduced_jacobian_indptr) + + # Make sure two rows are removed and deduce shape of reduced jacobian + assert ( + len(fully_reduced_jacobian_indptr) == len(self.reduced_jacobian.indptr) - 2 + ), "Two rows should be removed." + fully_reduced_jacobian_shape = ( + len(fully_reduced_jacobian_indptr) - 1, + len(fully_reduced_jacobian_indptr) - 1, + ) + + # Cache the fully reduced jacobian + self.fully_reduced_jacobian = sps.csc_matrix( + ( + fully_reduced_jacobian_data, + fully_reduced_jacobian_indices, + fully_reduced_jacobian_indptr, + ), + shape=fully_reduced_jacobian_shape, + ) + + # Cache the indices and indptr + self.fully_reduced_jacobian_indices = fully_reduced_jacobian_indices.copy() + self.fully_reduced_jacobian_indptr = fully_reduced_jacobian_indptr.copy() + self.fully_reduced_jacobian_shape = fully_reduced_jacobian_shape + + # Step 4: Identify inclusions (index arrays) + self.flux_indices = np.arange(self.num_faces) + self.potential_indices = np.arange( + self.num_faces, self.num_faces + self.num_cells + ) + self.lagrange_multiplier_indices = np.array( + [self.num_faces + self.num_cells], dtype=int + ) + + # Define reduced system indices wrt full system + self.reduced_system_indices = np.concatenate( + [self.potential_indices, self.lagrange_multiplier_indices] + ) + + # Define fully reduced system indices wrt reduced system - need to remove cell + # (and implicitly lagrange multiplier) + self.fully_reduced_system_indices = np.delete( + np.arange(self.num_cells), self.constrained_cell_flat_index + ) + + # Define fully reduced system indices wrt full system + self.fully_reduced_system_indices_full = self.reduced_system_indices[ + self.fully_reduced_system_indices + ] + + def remove_flux(self, jacobian: sps.csc_matrix, residual: np.ndarray) -> tuple: + """Remove the flux block from the jacobian and residual. + + Args: + jacobian (sps.csc_matrix): jacobian + residual (np.ndarray): residual + + Returns: + tuple: reduced jacobian, reduced residual, inverse of flux block + + """ + # Build Schur complement wrt flux-block + # TODO Speed up extraction of J, use infrastructure setup. + # TODO Just extract diaginal directly! + J = jacobian[: self.num_faces, : self.num_faces].copy() + J_inv = sps.diags(1.0 / J.diagonal()) + schur_complement = self.D.dot(J_inv.dot(self.DT)) + + # Gauss eliminiation on matrices + reduced_jacobian = self.jacobian_subblock + schur_complement + + # Gauss elimination on vectors + reduced_residual = residual[self.reduced_system_indices].copy() + reduced_residual -= self.D.dot(J_inv.dot(residual[self.flux_indices])) + + return reduced_jacobian, reduced_residual, J_inv + + def remove_lagrange_multiplier(self, jacobian, residual, solution) -> tuple: + """Shortcut for removing the lagrange multiplier from the reduced jacobian. + + Args: + + solution (np.ndarray): solution, TODO make function independent of solution + + Returns: + tuple: fully reduced jacobian, fully reduced residual + + """ + # Make sure the jacobian is a CSC matrix + assert isinstance(jacobian, sps.csc_matrix), "Jacobian should be a CSC matrix." + + # Effective Gauss-elimination for the particular case of the lagrange multiplier + self.fully_reduced_jacobian.data[:] = np.delete( + self.reduced_jacobian.data.copy(), self.rm_indices ) + # NOTE: The indices have to be restored if the LU factorization is to be used + # FIXME omit if not required + self.fully_reduced_jacobian.indices = self.fully_reduced_jacobian_indices.copy() + + # Rhs is not affected by Gauss elimination as it is assumed that the residual + # is zero in the constrained cell, and the pressure is zero there as well. + # If not, we need to do a proper Gauss elimination on the right hand side! + if abs(residual[-1]) > 1e-6: + raise NotImplementedError("Implementation requires residual to be zero.") + if abs(solution[self.num_faces + self.constrained_cell_flat_index]) > 1e-6: + raise NotImplementedError("Implementation requires solution to be zero.") + fully_reduced_residual = self.reduced_residual[ + self.fully_reduced_system_indices + ].copy() + + return self.fully_reduced_jacobian, fully_reduced_residual def linearization_step( self, solution: np.ndarray, rhs: np.ndarray, iter: int @@ -922,74 +1084,139 @@ def linearization_step( jacobian_lu = sps.linalg.splu(approx_jacobian) update = jacobian_lu.solve(residual) elif linear_solver == "amg": - # Build Schur complement wrt flux-block - J = approx_jacobian[: self.num_faces, : self.num_faces] - J_inv = sps.diags(1.0 / J.diagonal()) - D = approx_jacobian[self.num_faces :, : self.num_faces] - schur_complement = D.dot(J_inv.dot(D.T)) - - # Gauss eliminiation on matrices - self.reduced_jacobian.data[:] = 0.0 - self.reduced_jacobian += approx_jacobian[self.num_faces :, self.num_faces :] - self.reduced_jacobian += schur_complement + # Reduce flux block + ( + self.reduced_jacobian, + self.reduced_residual, + jacobian_flux_inv, + ) = self.remove_flux(approx_jacobian, residual) - # Gauss elimination on vectors - reduced_residual = residual[self.num_faces :] - reduced_residual -= D.dot(J_inv.dot(residual[: self.num_faces])) + # Reduce to pure pressure system + ( + self.fully_reduced_jacobian, + self.fully_reduced_residual, + ) = self.remove_lagrange_multiplier( + self.reduced_jacobian, self.reduced_residual, solution + ) - # TODO reduce to pure pressure system! + # Allocate update + update = np.zeros_like(solution, dtype=float) # ML architecture - ml = pyamg.smoothed_aggregation_solver( - self.reduced_jacobian, - # B=X.reshape( - # n * n, 1 - # ), # the representation of the near null space (this is a poor choice) - # BH=None, # the representation of the left near null space - symmetry="hermitian", # indicate that the matrix is Hermitian - # strength="evolution", # change the strength of connection - aggregate="standard", # use a standard aggregation method - smooth=( - "jacobi", - {"omega": 4.0 / 3.0, "degree": 2}, - ), # prolongation smoothing - presmoother=("block_gauss_seidel", {"sweep": "symmetric"}), - postsmoother=("block_gauss_seidel", {"sweep": "symmetric"}), - # improve_candidates=[ - # ("block_gauss_seidel", {"sweep": "symmetric", "iterations": 4}), - # None, - # ], - max_levels=4, # maximum number of levels - max_coarse=1000, # maximum number on a coarse level - # keep=False, # keep extra operators around in the hierarchy (memory) - ) if False: - print(ml) + ml = pyamg.smoothed_aggregation_solver( + self.reduced_jacobian, + # B=X.reshape( + # n * n, 1 + # ), # the representation of the near null space (this is a poor choice) + # BH=None, # the representation of the left near null space + symmetry="hermitian", # indicate that the matrix is Hermitian + # strength="evolution", # change the strength of connection + aggregate="standard", # use a standard aggregation method + smooth=( + "jacobi", + {"omega": 4.0 / 3.0, "degree": 2}, + ), # prolongation smoothing + presmoother=("block_gauss_seidel", {"sweep": "symmetric"}), + postsmoother=("block_gauss_seidel", {"sweep": "symmetric"}), + # improve_candidates=[ + # ("block_gauss_seidel", {"sweep": "symmetric", "iterations": 4}), + # None, + # ], + max_levels=4, # maximum number of levels + max_coarse=1000, # maximum number on a coarse level + # keep=False, # keep extra operators around in the hierarchy (memory) + ) + if False: + print(ml) - tol_linear_solver = self.options.get("tol_linear_solver", 1e-6) - res_history = [] - update = np.zeros_like(solution, dtype=float) - update[self.num_faces :] = ml.solve( - reduced_residual, tol=tol_linear_solver, residuals=res_history - ) - # TODO rm only for debugging - # lu = sps.linalg.splu(self.reduced_jacobian) - # update[self.num_faces :] = lu.solve(reduced_residual) - if False: - print( - "res: ", - len(res_history), - np.linalg.norm( - reduced_residual - - self.reduced_jacobian.dot(update[self.num_faces :]) - ), + tol_linear_solver = self.options.get("tol_linear_solver", 1e-6) + res_history = [] + update[self.reduced_system_indices] = ml.solve( + self.reduced_residual, tol=tol_linear_solver, residuals=res_history + ) + if False: + print( + "res: ", + len(res_history), + np.linalg.norm( + reduced_residual + - self.reduced_jacobian.dot(update[self.num_faces :]) + ), + ) + print("update", np.linalg.norm(update)) + + elif False: + # LU for simply reduced system + + # For debugging only - TODO rm + lu = sps.linalg.splu(self.reduced_jacobian) + update[self.reduced_system_indices] = lu.solve(self.reduced_residual) + elif True: + # AMG for fully reduced system + tic = time.time() + ml = pyamg.smoothed_aggregation_solver( + self.fully_reduced_jacobian, + # B=X.reshape( + # n * n, 1 + # ), # the representation of the near null space (this is a poor choice) + # BH=None, # the representation of the left near null space + symmetry="hermitian", # indicate that the matrix is Hermitian + # strength="evolution", # change the strength of connection + aggregate="standard", # use a standard aggregation method + smooth=( + "jacobi", + {"omega": 4.0 / 3.0, "degree": 2}, + ), # prolongation smoothing + presmoother=("block_gauss_seidel", {"sweep": "symmetric"}), + postsmoother=("block_gauss_seidel", {"sweep": "symmetric"}), + # improve_candidates=[ + # ("block_gauss_seidel", {"sweep": "symmetric", "iterations": 4}), + # None, + # ], + max_levels=4, # maximum number of levels + max_coarse=1000, # maximum number on a coarse level + # keep=False, # keep extra operators around in the hierarchy (memory) + ) + print("setup ml", time.time() - tic) + if False: + print(ml) + + tol_linear_solver = self.options.get("tol_linear_solver", 1e-6) + res_history = [] + tic = time.time() + update[self.fully_reduced_system_indices_full] = ml.solve( + self.fully_reduced_residual, + tol=tol_linear_solver, + residuals=res_history, + ) + print("ml solve", time.time() - tic) + if False: + print( + "res: ", + len(res_history), + np.linalg.norm( + reduced_residual + - self.reduced_jacobian.dot( + update[self.reduced_system_indices] + ) + ), + ) + print("update", np.linalg.norm(update)) + + else: + # LU for fully reduced system + lu = sps.linalg.splu(self.fully_reduced_jacobian) + update[self.fully_reduced_system_indices_full] = lu.solve( + self.fully_reduced_residual ) - print("update", np.linalg.norm(update)) # Compute flux update - update[: self.num_faces] = J_inv.dot( - residual[: self.num_faces] + D.T.dot(update[self.num_faces :]) + update[self.flux_indices] = jacobian_flux_inv.dot( + residual[self.flux_indices] + + self.DT.dot(update[self.reduced_system_indices]) ) + toc = time.time() time_solve = toc - tic stats = [time_setup, time_solve] @@ -1095,7 +1322,7 @@ def _solve(self, flat_mass_diff: np.ndarray) -> tuple[float, np.ndarray, dict]: # - residual of the constraint equation # - full increment # - flux increment - # - pressure increment + # - potential increment # - lagrange multiplier increment # - distance increment increment = solution_i - old_solution_i From f71c1f6a115d0a5cb69bed32a45e39072e4b1c1a Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Sat, 2 Sep 2023 21:10:50 +0200 Subject: [PATCH 018/100] DOC Improve documentation --- src/darsia/measure/wasserstein.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index f941a084..603975af 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -485,7 +485,11 @@ def cell_reconstruction(self, flat_flux: np.ndarray) -> np.ndarray: return cell_flux def face_restriction(self, cell_flux: np.ndarray) -> np.ndarray: - """Restrict the fluxes on the cells to the faces. + """Restrict vector-valued fluxes on cells to normal components on faces. + + Matrix-free implementation. The fluxes on the faces are determined by + arithmetic averaging of the fluxes on the cells in the direction of the normal + of the face. Args: cell_flux (np.ndarray): cell-based fluxes @@ -494,9 +498,7 @@ def face_restriction(self, cell_flux: np.ndarray) -> np.ndarray: np.ndarray: face-based fluxes """ - # TODO replace by sparse matrix multiplication - - # Determine the fluxes on the faces + # Determine the fluxes on the faces through arithmetic averaging horizontal_fluxes = 0.5 * (cell_flux[:, :-1, 0] + cell_flux[:, 1:, 0]) vertical_fluxes = 0.5 * (cell_flux[:-1, :, 1] + cell_flux[1:, :, 1]) From e2e52c352007f28c600b6c0c00cb7bbe16a14f60 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Sat, 2 Sep 2023 21:13:24 +0200 Subject: [PATCH 019/100] DOC: improve documentation --- src/darsia/measure/wasserstein.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index 603975af..f8e29450 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -456,6 +456,13 @@ def split_solution( def cell_reconstruction(self, flat_flux: np.ndarray) -> np.ndarray: """Reconstruct the fluxes on the cells from the fluxes on the faces. + Use the Raviart-Thomas reconstruction of the fluxes on the cells from the fluxes + on the faces, and use arithmetic averaging of the fluxes on the faces, + equivalent with the L2 projection of the fluxes on the faces to the fluxes on + the cells. + + Matrix-free implementation. + Args: flat_flux (np.ndarray): flat fluxes (normal fluxes on the faces) @@ -463,8 +470,6 @@ def cell_reconstruction(self, flat_flux: np.ndarray) -> np.ndarray: np.ndarray: cell-based vectorial fluxes """ - # TODO replace by sparse matrix multiplication - # Reshape fluxes - use duality of faces and normals horizontal_fluxes = flat_flux[: self.num_faces_axis[0]].reshape( self.vertical_faces_shape @@ -473,7 +478,8 @@ def cell_reconstruction(self, flat_flux: np.ndarray) -> np.ndarray: self.horizontal_faces_shape ) - # Determine a cell-based Raviart-Thomas reconstruction of the fluxes + # Determine a cell-based Raviart-Thomas reconstruction of the fluxes, projected + # onto piecewise constant functions. cell_flux = np.zeros((*self.dim_cells, self.dim), dtype=float) # Horizontal fluxes cell_flux[:, :-1, 0] += 0.5 * horizontal_fluxes From c82b5a9c3ec3da6b3119285070b991b32761caaf Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Sat, 2 Sep 2023 23:53:36 +0200 Subject: [PATCH 020/100] ENH: Add two variants to project cell to face data. --- src/darsia/measure/wasserstein.py | 41 ++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index f8e29450..0e15bae7 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -515,20 +515,49 @@ def face_restriction(self, cell_flux: np.ndarray) -> np.ndarray: return flat_flux - def face_restriction_scalar(self, cell_qty: np.ndarray) -> np.ndarray: - """Restrict the fluxes on the cells to the faces. + def cell_to_face(self, cell_qty: np.ndarray, mode: str) -> np.ndarray: + """Project scalar cell quantity to scalr face quantity. + + Allow for arithmetic or harmonic averaging of the cell quantity to the faces. In + the harmonic case, the averaging is regularized to avoid division by zero. + Matrix-free implementation. Args: - cell_qty (np.ndarray): cell-based quantity + cell_qty (np.ndarray): scalar-valued cell-based quantity + mode (str): mode of projection, either "arithmetic" or "harmonic" + (averaging) Returns: np.ndarray: face-based quantity """ # Determine the fluxes on the faces + if mode == "arithmetic": + # Employ arithmetic averaging + horizontal_face_qty = 0.5 * (cell_qty[:, :-1] + cell_qty[:, 1:]) + vertical_face_qty = 0.5 * (cell_qty[:-1, :] + cell_qty[1:, :]) + elif mode == "harmonic": + # Employ harmonic averaging + arithmetic_avg_horizontal = 0.5 * (cell_qty[:, :-1] + cell_qty[:, 1:]) + arithmetic_avg_vertical = 0.5 * (cell_qty[:-1, :] + cell_qty[1:, :]) + # Regularize to avoid division by zero + regularization = 1e-10 + arithmetic_avg_horizontal = ( + arithmetic_avg_horizontal + + (2 * np.sign(arithmetic_avg_horizontal) + 1) * regularization + ) + arithmetic_avg_vertical = ( + 0.5 * arithmetic_avg_vertical + + (2 * np.sign(arithmetic_avg_vertical) + 1) * regularization + ) + product_horizontal = np.multiply(cell_qty[:, :-1], cell_qty[:, 1:]) + product_vertical = np.multiply(cell_qty[:-1, :], cell_qty[1:, :]) - horizontal_face_qty = 0.5 * (cell_qty[:, :-1] + cell_qty[:, 1:]) - vertical_face_qty = 0.5 * (cell_qty[:-1, :] + cell_qty[1:, :]) + # Determine the harmonic average + horizontal_face_qty = product_horizontal / arithmetic_avg_horizontal + vertical_face_qty = product_vertical / arithmetic_avg_vertical + else: + raise ValueError("Mode not supported.") # Reshape the fluxes - hardcoding the connectivity here face_qty = np.concatenate( @@ -1454,7 +1483,7 @@ def _solve(self, flat_mass_diff): cell_scaling = np.maximum(norm - 1 / self.L, 0) / ( norm + self.regularization ) - flat_scaling = self.face_restriction_scalar(cell_scaling, mode="arithmetic") + flat_scaling = self.cell_to_face(cell_scaling, mode="arithmetic") new_flat_flux_i = flat_scaling * intermediate_flat_flux_i # Apply Anderson acceleration to flux contribution (the only nonlinear part). From d898b2320dd6640c6b3a5ed042a0d64bd6761190 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Sat, 2 Sep 2023 23:58:12 +0200 Subject: [PATCH 021/100] MAINT: Rename method --- src/darsia/measure/wasserstein.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index 0e15bae7..33ff1257 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -453,7 +453,7 @@ def split_solution( # ! ---- Projections inbetween faces and cells ---- - def cell_reconstruction(self, flat_flux: np.ndarray) -> np.ndarray: + def face_to_cell(self, flat_flux: np.ndarray) -> np.ndarray: """Reconstruct the fluxes on the cells from the fluxes on the faces. Use the Raviart-Thomas reconstruction of the fluxes on the cells from the fluxes @@ -591,7 +591,7 @@ def l1_dissipation(self, solution: np.ndarray) -> float: """ # TODO use improved quadrature? flat_flux, _, _ = self.split_solution(solution) - cell_flux = self.cell_reconstruction(flat_flux) + cell_flux = self.face_to_cell(flat_flux) return np.sum(np.prod(self.voxel_size) * np.linalg.norm(cell_flux, 2, axis=-1)) # ! ---- Lumping of effective mobility @@ -614,20 +614,16 @@ def face_flux_norm(self, solution: np.ndarray, mode: str) -> np.ndarray: # Determine the norm of the fluxes on the faces if mode in ["cell_arithmetic", "cell_harmonic"]: # Consider the piecewise constant projection of vector valued fluxes - cell_flux = self.cell_reconstruction(flat_flux) + cell_flux = self.face_to_cell(flat_flux) # Determine the norm of the fluxes on the cells cell_flux_norm = np.maximum( np.linalg.norm(cell_flux, 2, axis=-1), self.regularization ) # Take the average over faces if mode == "cell_arithmetic": - flat_flux_norm = self.face_restriction_scalar( - cell_flux_norm, mode="arithmetic" - ) + flat_flux_norm = self.cell_to_face(cell_flux_norm, mode="arithmetic") elif mode == "cell_harmonic": - flat_flux_norm = self.face_restriction_scalar( - cell_flux_norm, mode="harmonic" - ) + flat_flux_norm = self.cell_to_face(cell_flux_norm, mode="harmonic") elif mode == "face_arithmetic": # Define natural vector valued flux on faces (taking averages over cells) tangential_flux = self.orthogonal_face_average.dot(flat_flux) @@ -681,7 +677,7 @@ def __call__( flat_flux, flat_potential, _ = self.split_solution(solution) # Reshape the fluxes and potential to grid format - flux = self.cell_reconstruction(flat_flux) + flux = self.face_to_cell(flat_flux) potential = flat_potential.reshape(self.dim_cells) # Determine transport density @@ -832,8 +828,8 @@ def residual(self, rhs: np.ndarray, solution: np.ndarray) -> np.ndarray: """ flat_flux, _, _ = self.split_solution(solution) - # TODO cell-based version meaningful? - # cell_flux = self.cell_reconstruction(flat_flux) + # TODO cell-based version meaningful? rm + # cell_flux = self.face_to_cell(flat_flux) # cell_flux_norm = np.maximum( # np.linalg.norm(cell_flux, 2, axis=-1), self.regularization # ) @@ -1476,9 +1472,7 @@ def _solve(self, flat_mass_diff): # new_flat_flux_i = np.sign(intermediate_flat_flux_i) * ( # np.maximum(np.abs(intermediate_flat_flux_i) - 1.0 / self.L, 0.0) # ) - cell_intermediate_flux_i = self.cell_reconstruction( - intermediate_flat_flux_i - ) + cell_intermediate_flux_i = self.face_to_cell(intermediate_flat_flux_i) norm = np.linalg.norm(cell_intermediate_flux_i, 2, axis=-1) cell_scaling = np.maximum(norm - 1 / self.L, 0) / ( norm + self.regularization From ffe8abe12920a9f34622d6100337e744727812b6 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Sun, 3 Sep 2023 00:01:23 +0200 Subject: [PATCH 022/100] MAINT: Change signature --- src/darsia/measure/wasserstein.py | 66 +++++++++++++++---------------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index 33ff1257..547ffc59 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -490,31 +490,6 @@ def face_to_cell(self, flat_flux: np.ndarray) -> np.ndarray: return cell_flux - def face_restriction(self, cell_flux: np.ndarray) -> np.ndarray: - """Restrict vector-valued fluxes on cells to normal components on faces. - - Matrix-free implementation. The fluxes on the faces are determined by - arithmetic averaging of the fluxes on the cells in the direction of the normal - of the face. - - Args: - cell_flux (np.ndarray): cell-based fluxes - - Returns: - np.ndarray: face-based fluxes - - """ - # Determine the fluxes on the faces through arithmetic averaging - horizontal_fluxes = 0.5 * (cell_flux[:, :-1, 0] + cell_flux[:, 1:, 0]) - vertical_fluxes = 0.5 * (cell_flux[:-1, :, 1] + cell_flux[1:, :, 1]) - - # Reshape the fluxes - flat_flux = np.concatenate( - [horizontal_fluxes.ravel(), vertical_fluxes.ravel()], axis=0 - ) - - return flat_flux - def cell_to_face(self, cell_qty: np.ndarray, mode: str) -> np.ndarray: """Project scalar cell quantity to scalr face quantity. @@ -566,6 +541,31 @@ def cell_to_face(self, cell_qty: np.ndarray, mode: str) -> np.ndarray: return face_qty + def face_restriction(self, cell_flux: np.ndarray) -> np.ndarray: + """Restrict vector-valued fluxes on cells to normal components on faces. + + Matrix-free implementation. The fluxes on the faces are determined by + arithmetic averaging of the fluxes on the cells in the direction of the normal + of the face. + + Args: + cell_flux (np.ndarray): cell-based fluxes + + Returns: + np.ndarray: face-based fluxes + + """ + # Determine the fluxes on the faces through arithmetic averaging + horizontal_fluxes = 0.5 * (cell_flux[:, :-1, 0] + cell_flux[:, 1:, 0]) + vertical_fluxes = 0.5 * (cell_flux[:-1, :, 1] + cell_flux[1:, :, 1]) + + # Reshape the fluxes + flat_flux = np.concatenate( + [horizontal_fluxes.ravel(), vertical_fluxes.ravel()], axis=0 + ) + + return flat_flux + # ! ---- Effective quantities ---- def transport_density(self, cell_flux: np.ndarray) -> np.ndarray: @@ -596,11 +596,11 @@ def l1_dissipation(self, solution: np.ndarray) -> float: # ! ---- Lumping of effective mobility - def face_flux_norm(self, solution: np.ndarray, mode: str) -> np.ndarray: - """Compute the norm of the fluxes on the faces. + def face_flux_norm(self, flat_flux: np.ndarray, mode: str) -> np.ndarray: + """Compute the norm of the vector-valued fluxes on the faces. Args: - solution (np.ndarray): solution + flat_flux (np.ndarray): flat fluxes (normal fluxes on the faces) mode (str): mode of the norm Returns: @@ -608,9 +608,6 @@ def face_flux_norm(self, solution: np.ndarray, mode: str) -> np.ndarray: """ - # Extract the fluxes - flat_flux, _, _ = self.split_solution(solution) - # Determine the norm of the fluxes on the faces if mode in ["cell_arithmetic", "cell_harmonic"]: # Consider the piecewise constant projection of vector valued fluxes @@ -836,9 +833,9 @@ def residual(self, rhs: np.ndarray, solution: np.ndarray) -> np.ndarray: # cell_flux_normed = cell_flux / cell_flux_norm[..., None] # flat_flux_normed = self.face_restriction(cell_flux_normed) mode = "face_arithmetic" - #mode = "cell_arithmetic" + # mode = "cell_arithmetic" flat_flux_norm = np.maximum( - self.face_flux_norm(solution, mode=mode), self.regularization + self.face_flux_norm(flat_flux, mode=mode), self.regularization ) flat_flux_normed = flat_flux / flat_flux_norm @@ -860,8 +857,9 @@ def jacobian(self, solution: np.ndarray) -> sps.linalg.LinearOperator: """ mode = "face_arithmetic" # mode = "cell_arithmetic" + flat_flux, _, _ = self.split_solution(solution) flat_flux_norm = np.maximum( - self.face_flux_norm(solution, mode=mode), self.regularization + self.face_flux_norm(flat_flux, mode=mode), self.regularization ) approx_jacobian = sps.bmat( [ From 13e3cf825c1e8bbcaebbf42914a069a365b14235 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Sun, 3 Sep 2023 11:28:47 +0200 Subject: [PATCH 023/100] MAINT: Improve doc, naming --- src/darsia/measure/wasserstein.py | 39 +++++++++++++++++-------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index 547ffc59..38340fe3 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -202,14 +202,14 @@ def _setup(self) -> None: np.prod(self.voxel_size) * np.ones(num_cells, dtype=float) ) - # Define sparse mass matrix on faces: flat_fluxes -> flat_fluxes + # Define sparse mass matrix on faces: flat fluxes -> flat fluxes lumping = self.options.get("lumping", True) if lumping: self.mass_matrix_faces = sps.diags( np.prod(self.voxel_size) * np.ones(num_faces, dtype=float) ) else: - # Define true RT0 mass matrix on faces: flat_fluxes -> flat_fluxes + # Define true RT0 mass matrix on faces: flat fluxes -> flat fluxes mass_matrix_faces_shape = (num_faces, num_faces) mass_matrix_faces_data = np.prod(self.voxel_size) * np.concatenate( ( @@ -435,7 +435,7 @@ def _problem_specific_setup(self, mass_diff: np.ndarray) -> None: def split_solution( self, solution: np.ndarray ) -> tuple[np.ndarray, np.ndarray, float]: - """Split the solution into fluxes, potential and lagrange multiplier. + """Split the solution into (flat) fluxes, potential and lagrange multiplier. Args: solution (np.ndarray): solution @@ -596,15 +596,18 @@ def l1_dissipation(self, solution: np.ndarray) -> float: # ! ---- Lumping of effective mobility - def face_flux_norm(self, flat_flux: np.ndarray, mode: str) -> np.ndarray: + def vector_face_flux_norm(self, flat_flux: np.ndarray, mode: str) -> np.ndarray: """Compute the norm of the vector-valued fluxes on the faces. Args: flat_flux (np.ndarray): flat fluxes (normal fluxes on the faces) - mode (str): mode of the norm + mode (str): mode of the norm, either "cell_arithmetic", "cell_harmonic" or + "face_arithmetic". In the cell-based modes, the fluxes are projected to + the cells and the norm is computed there. In the face-based mode, the + norm is computed directly on the faces. Returns: - np.ndarray: norm of the fluxes on the faces + np.ndarray: norm of the vector-valued fluxes on the faces """ @@ -616,16 +619,16 @@ def face_flux_norm(self, flat_flux: np.ndarray, mode: str) -> np.ndarray: cell_flux_norm = np.maximum( np.linalg.norm(cell_flux, 2, axis=-1), self.regularization ) - # Take the average over faces - if mode == "cell_arithmetic": - flat_flux_norm = self.cell_to_face(cell_flux_norm, mode="arithmetic") - elif mode == "cell_harmonic": - flat_flux_norm = self.cell_to_face(cell_flux_norm, mode="harmonic") + # Determine averaging mode from mode - either arithmetic or harmonic + average_mode = mode.split("_")[1] + flat_flux_norm = self.cell_to_face(cell_flux_norm, mode=average_mode) + elif mode == "face_arithmetic": # Define natural vector valued flux on faces (taking averages over cells) tangential_flux = self.orthogonal_face_average.dot(flat_flux) # Determine the l2 norm of the fluxes on the faces, add some regularization flat_flux_norm = np.sqrt(flat_flux**2 + tangential_flux**2) + else: raise ValueError(f"Mode {mode} not supported.") @@ -835,7 +838,7 @@ def residual(self, rhs: np.ndarray, solution: np.ndarray) -> np.ndarray: mode = "face_arithmetic" # mode = "cell_arithmetic" flat_flux_norm = np.maximum( - self.face_flux_norm(flat_flux, mode=mode), self.regularization + self.vector_face_flux_norm(flat_flux, mode=mode), self.regularization ) flat_flux_normed = flat_flux / flat_flux_norm @@ -859,7 +862,7 @@ def jacobian(self, solution: np.ndarray) -> sps.linalg.LinearOperator: # mode = "cell_arithmetic" flat_flux, _, _ = self.split_solution(solution) flat_flux_norm = np.maximum( - self.face_flux_norm(flat_flux, mode=mode), self.regularization + self.vector_face_flux_norm(flat_flux, mode=mode), self.regularization ) approx_jacobian = sps.bmat( [ @@ -1112,6 +1115,7 @@ def linearization_step( # Solve linear system for the update if linear_solver == "lu": + # LU for full system jacobian_lu = sps.linalg.splu(approx_jacobian) update = jacobian_lu.solve(residual) elif linear_solver == "amg": @@ -1158,7 +1162,8 @@ def linearization_step( max_coarse=1000, # maximum number on a coarse level # keep=False, # keep extra operators around in the hierarchy (memory) ) - if False: + + if self.options.get("linear_solver_verbosity_ml", False): print(ml) tol_linear_solver = self.options.get("tol_linear_solver", 1e-6) @@ -1166,9 +1171,9 @@ def linearization_step( update[self.reduced_system_indices] = ml.solve( self.reduced_residual, tol=tol_linear_solver, residuals=res_history ) - if False: + if self.options.get("linear_solver_verbosity", False): print( - "res: ", + "Residual after ML step: ", len(res_history), np.linalg.norm( reduced_residual @@ -1183,7 +1188,7 @@ def linearization_step( # For debugging only - TODO rm lu = sps.linalg.splu(self.reduced_jacobian) update[self.reduced_system_indices] = lu.solve(self.reduced_residual) - elif True: + elif False: # AMG for fully reduced system tic = time.time() ml = pyamg.smoothed_aggregation_solver( From 61abe8e92325d39c7c72a7ef74ca59016c589b24 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Sun, 3 Sep 2023 11:29:17 +0200 Subject: [PATCH 024/100] MAINT: Exclude for now redundant routine, and add some keywords. --- src/darsia/measure/wasserstein.py | 51 ++++++++++++++++--------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index 38340fe3..a8c92c5d 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -541,30 +541,31 @@ def cell_to_face(self, cell_qty: np.ndarray, mode: str) -> np.ndarray: return face_qty - def face_restriction(self, cell_flux: np.ndarray) -> np.ndarray: - """Restrict vector-valued fluxes on cells to normal components on faces. - - Matrix-free implementation. The fluxes on the faces are determined by - arithmetic averaging of the fluxes on the cells in the direction of the normal - of the face. - - Args: - cell_flux (np.ndarray): cell-based fluxes - - Returns: - np.ndarray: face-based fluxes - - """ - # Determine the fluxes on the faces through arithmetic averaging - horizontal_fluxes = 0.5 * (cell_flux[:, :-1, 0] + cell_flux[:, 1:, 0]) - vertical_fluxes = 0.5 * (cell_flux[:-1, :, 1] + cell_flux[1:, :, 1]) - - # Reshape the fluxes - flat_flux = np.concatenate( - [horizontal_fluxes.ravel(), vertical_fluxes.ravel()], axis=0 - ) - - return flat_flux + # NOTE: Currently not in use. TODO rm? + # def face_restriction(self, cell_flux: np.ndarray) -> np.ndarray: + # """Restrict vector-valued fluxes on cells to normal components on faces. + # + # Matrix-free implementation. The fluxes on the faces are determined by + # arithmetic averaging of the fluxes on the cells in the direction of the normal + # of the face. + # + # Args: + # cell_flux (np.ndarray): cell-based fluxes + # + # Returns: + # np.ndarray: face-based fluxes + # + # """ + # # Determine the fluxes on the faces through arithmetic averaging + # horizontal_fluxes = 0.5 * (cell_flux[:, :-1, 0] + cell_flux[:, 1:, 0]) + # vertical_fluxes = 0.5 * (cell_flux[:-1, :, 1] + cell_flux[1:, :, 1]) + # + # # Reshape the fluxes + # flat_flux = np.concatenate( + # [horizontal_fluxes.ravel(), vertical_fluxes.ravel()], axis=0 + # ) + # + # return flat_flux # ! ---- Effective quantities ---- @@ -696,6 +697,7 @@ def __call__( else: return distance + # TODO rm. def _plot_solution( self, mass_diff: np.ndarray, @@ -864,6 +866,7 @@ def jacobian(self, solution: np.ndarray) -> sps.linalg.LinearOperator: flat_flux_norm = np.maximum( self.vector_face_flux_norm(flat_flux, mode=mode), self.regularization ) + # TODO more efficient assemble approx_jacobian = sps.bmat( [ [ From 9701661119fcadf3c1b8f3faa8d0e9f9a40904f0 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Sun, 3 Sep 2023 11:37:52 +0200 Subject: [PATCH 025/100] MAINT: More precise error message --- src/darsia/measure/wasserstein.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index a8c92c5d..3c59b539 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -532,7 +532,7 @@ def cell_to_face(self, cell_qty: np.ndarray, mode: str) -> np.ndarray: horizontal_face_qty = product_horizontal / arithmetic_avg_horizontal vertical_face_qty = product_vertical / arithmetic_avg_vertical else: - raise ValueError("Mode not supported.") + raise ValueError(f"Mode {mode} not supported.") # Reshape the fluxes - hardcoding the connectivity here face_qty = np.concatenate( From 63663973670e348f9682f26980a4f10cd929149f Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Sun, 3 Sep 2023 11:55:54 +0200 Subject: [PATCH 026/100] MAINT: Improve doc, and move code, improve efficiency. --- src/darsia/measure/wasserstein.py | 51 ++++++++++++++----------------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index 3c59b539..74e3c964 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -13,6 +13,11 @@ import darsia +# General TODO list +# - improve documentation, in particular with focus on keywords +# - remove plotting +# - improve assembling of operators through partial assembling + class VariationalWassersteinDistance(darsia.EMD): """Base class for setting up the variational Wasserstein distance. @@ -55,7 +60,6 @@ def __init__( - lumping (bool): lump the mass matrix. Defaults to True. """ - # TODO improve documentation for options - method dependent # Cache geometrical infos self.shape = shape self.voxel_size = voxel_size @@ -379,7 +383,10 @@ def _setup(self) -> None: shape=orthogonal_face_average_shape, ) - # Define sparse embedding operator for fluxes into full discrete DOF space + # Define sparse embedding operators, and quick access through indices. + # Assume the ordering of the faces is vertical faces first, then horizontal + # faces. After that, cell variabes are provided for the potential, finally a + # scalar variable for the lagrange multiplier. self.flux_embedding = sps.csc_matrix( ( np.ones(num_faces, dtype=float), @@ -388,6 +395,10 @@ def _setup(self) -> None: shape=(num_faces + num_cells + 1, num_faces), ) + self.flux_indices = np.arange(num_faces) + self.potential_indices = np.arange(num_faces, num_faces + num_cells) + self.lagrange_multiplier_indices = np.array([num_faces + num_cells], dtype=int) + # ! ---- Utilities ---- aa_depth = self.options.get("aa_depth", 0) aa_restart = self.options.get("aa_restart", None) @@ -830,15 +841,7 @@ def residual(self, rhs: np.ndarray, solution: np.ndarray) -> np.ndarray: """ flat_flux, _, _ = self.split_solution(solution) - # TODO cell-based version meaningful? rm - # cell_flux = self.face_to_cell(flat_flux) - # cell_flux_norm = np.maximum( - # np.linalg.norm(cell_flux, 2, axis=-1), self.regularization - # ) - # cell_flux_normed = cell_flux / cell_flux_norm[..., None] - # flat_flux_normed = self.face_restriction(cell_flux_normed) - mode = "face_arithmetic" - # mode = "cell_arithmetic" + mode = self.options.get("mode", "face_arithmetic") flat_flux_norm = np.maximum( self.vector_face_flux_norm(flat_flux, mode=mode), self.regularization ) @@ -860,13 +863,11 @@ def jacobian(self, solution: np.ndarray) -> sps.linalg.LinearOperator: sps.linalg.splu: LU factorization of the jacobian """ - mode = "face_arithmetic" - # mode = "cell_arithmetic" flat_flux, _, _ = self.split_solution(solution) + mode = self.options.get("mode", "face_arithmetic") flat_flux_norm = np.maximum( self.vector_face_flux_norm(flat_flux, mode=mode), self.regularization ) - # TODO more efficient assemble approx_jacobian = sps.bmat( [ [ @@ -883,7 +884,12 @@ def jacobian(self, solution: np.ndarray) -> sps.linalg.LinearOperator: return approx_jacobian def darcy_jacobian(self) -> sps.linalg.LinearOperator: - """Compute the LU factorization of the jacobian of the solution.""" + """Compute the Jacobian of a standard homogeneous Darcy problem. + + The mobility is defined by the used via options["L_init"]. The jacobian is + cached for later use. + + """ L_init = self.options.get("L_init", 1.0) jacobian = sps.bmat( [ @@ -909,8 +915,7 @@ def setup_infrastructure(self) -> None: # Step 2: Remove flux blocks through Schur complement approach # Build Schur complement wrt. flux-flux block - J = jacobian[: self.num_faces, : self.num_faces].copy() - J_inv = sps.diags(1.0 / J.diagonal()) + J_inv = sps.diags(1.0 / jacobian.diagonal()[self.flux_indices]) D = jacobian[self.num_faces :, : self.num_faces].copy() schur_complement = D.dot(J_inv.dot(D.T)) @@ -995,13 +1000,6 @@ def setup_infrastructure(self) -> None: self.fully_reduced_jacobian_shape = fully_reduced_jacobian_shape # Step 4: Identify inclusions (index arrays) - self.flux_indices = np.arange(self.num_faces) - self.potential_indices = np.arange( - self.num_faces, self.num_faces + self.num_cells - ) - self.lagrange_multiplier_indices = np.array( - [self.num_faces + self.num_cells], dtype=int - ) # Define reduced system indices wrt full system self.reduced_system_indices = np.concatenate( @@ -1031,10 +1029,7 @@ def remove_flux(self, jacobian: sps.csc_matrix, residual: np.ndarray) -> tuple: """ # Build Schur complement wrt flux-block - # TODO Speed up extraction of J, use infrastructure setup. - # TODO Just extract diaginal directly! - J = jacobian[: self.num_faces, : self.num_faces].copy() - J_inv = sps.diags(1.0 / J.diagonal()) + J_inv = sps.diags(1.0 / jacobian.diagonal()[self.flux_indices]) schur_complement = self.D.dot(J_inv.dot(self.DT)) # Gauss eliminiation on matrices From fa297cd438e48c70768e653d66eaea3b35c62911 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Sun, 3 Sep 2023 12:34:58 +0200 Subject: [PATCH 027/100] MAINT: Restructure linear solver allowing for external control --- src/darsia/measure/wasserstein.py | 227 +++++++++++++++--------------- 1 file changed, 111 insertions(+), 116 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index 74e3c964..c85194aa 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -698,6 +698,7 @@ def __call__( # Stop taking time toc = time.time() status["elapsed_time"] = toc - tic + print("Elapsed time: ", toc - tic) # Plot the solution if plot_solution: @@ -1093,31 +1094,108 @@ def linearization_step( tuple: update, residual, stats (timinings) """ + # Determine residual and (full) Jacobian tic = time.time() - # Determine residual if iter == 0: residual = rhs.copy() - else: - residual = self.residual(rhs, solution) - - # Setup linear solver - linear_solver = self.options.get("linear_solver", "lu") - if iter == 0: approx_jacobian = self.darcy_jacobian() else: + residual = self.residual(rhs, solution) approx_jacobian = self.jacobian(solution) - toc = time.time() time_setup = toc - tic + + # Allocate update + update = np.zeros_like(solution, dtype=float) + + # Setup linear solver tic = time.time() + linear_solver = self.options.get("linear_solver", "lu") + assert linear_solver in [ + "lu", + "lu-flux-reduced", + "amg-flux-reduced", + "lu-potential", + "amg-potential", + ], f"Linear solver {linear_solver} not supported." + + if linear_solver in ["amg-flux-reduced", "amg-potential"]: + # TODO add possibility for user control + ml_options = { + # B=X.reshape( + # n * n, 1 + # ), # the representation of the near null space (this is a poor choice) + # BH=None, # the representation of the left near null space + "symmetry": "hermitian", # indicate that the matrix is Hermitian + # strength="evolution", # change the strength of connection + "aggregate": "standard", # use a standard aggregation method + "smooth": ( + "jacobi", + {"omega": 4.0 / 3.0, "degree": 2}, + ), # prolongation smoothing + "presmoother": ("block_gauss_seidel", {"sweep": "symmetric"}), + "postsmoother": ("block_gauss_seidel", {"sweep": "symmetric"}), + # improve_candidates=[ + # ("block_gauss_seidel", {"sweep": "symmetric", "iterations": 4}), + # None, + # ], + "max_levels": 4, # maximum number of levels + "max_coarse": 1000, # maximum number on a coarse level + # keep=False, # keep extra operators around in the hierarchy (memory) + } + tol_amg = self.options.get("linear_solver_tol", 1e-6) + res_history_amg = [] # Solve linear system for the update if linear_solver == "lu": - # LU for full system + # Solve full system + tic = time.time() jacobian_lu = sps.linalg.splu(approx_jacobian) + time_setup = time.time() - tic + tic = time.time() update = jacobian_lu.solve(residual) - elif linear_solver == "amg": + time_solve = time.time() - tic + elif linear_solver in ["lu-flux-reduced", "amg-flux-reduced"]: + # Solve potential-multiplier problem + + # Reduce flux block + tic = time.time() + ( + self.reduced_jacobian, + self.reduced_residual, + jacobian_flux_inv, + ) = self.remove_flux(approx_jacobian, residual) + + if linear_solver == "lu-flux-reduced": + lu = sps.linalg.splu(self.reduced_jacobian) + time_setup = time.time() - tic + tic = time.time() + update[self.reduced_system_indices] = lu.solve(self.reduced_residual) + + elif linear_solver == "amg-flux-reduced": + ml = pyamg.smoothed_aggregation_solver( + self.reduced_jacobian, **ml_options + ) + time_setup = time.time() - tic + tic = time.time() + update[self.reduced_system_indices] = ml.solve( + self.reduced_residual, + tol=tol_amg, + residuals=res_history_amg, + ) + + # Compute flux update + update[self.flux_indices] = jacobian_flux_inv.dot( + residual[self.flux_indices] + + self.DT.dot(update[self.reduced_system_indices]) + ) + time_solve = time.time() - tic + + elif linear_solver in ["lu-potential", "amg-potential"]: + # Solve pure potential problem + # Reduce flux block + tic = time.time() ( self.reduced_jacobian, self.reduced_residual, @@ -1132,117 +1210,24 @@ def linearization_step( self.reduced_jacobian, self.reduced_residual, solution ) - # Allocate update - update = np.zeros_like(solution, dtype=float) - - # ML architecture - if False: - ml = pyamg.smoothed_aggregation_solver( - self.reduced_jacobian, - # B=X.reshape( - # n * n, 1 - # ), # the representation of the near null space (this is a poor choice) - # BH=None, # the representation of the left near null space - symmetry="hermitian", # indicate that the matrix is Hermitian - # strength="evolution", # change the strength of connection - aggregate="standard", # use a standard aggregation method - smooth=( - "jacobi", - {"omega": 4.0 / 3.0, "degree": 2}, - ), # prolongation smoothing - presmoother=("block_gauss_seidel", {"sweep": "symmetric"}), - postsmoother=("block_gauss_seidel", {"sweep": "symmetric"}), - # improve_candidates=[ - # ("block_gauss_seidel", {"sweep": "symmetric", "iterations": 4}), - # None, - # ], - max_levels=4, # maximum number of levels - max_coarse=1000, # maximum number on a coarse level - # keep=False, # keep extra operators around in the hierarchy (memory) - ) - - if self.options.get("linear_solver_verbosity_ml", False): - print(ml) - - tol_linear_solver = self.options.get("tol_linear_solver", 1e-6) - res_history = [] - update[self.reduced_system_indices] = ml.solve( - self.reduced_residual, tol=tol_linear_solver, residuals=res_history + if linear_solver == "lu-potential": + lu = sps.linalg.splu(self.fully_reduced_jacobian) + time_setup = time.time() - tic + tic = time.time() + update[self.fully_reduced_system_indices_full] = lu.solve( + self.fully_reduced_residual ) - if self.options.get("linear_solver_verbosity", False): - print( - "Residual after ML step: ", - len(res_history), - np.linalg.norm( - reduced_residual - - self.reduced_jacobian.dot(update[self.num_faces :]) - ), - ) - print("update", np.linalg.norm(update)) - elif False: - # LU for simply reduced system - - # For debugging only - TODO rm - lu = sps.linalg.splu(self.reduced_jacobian) - update[self.reduced_system_indices] = lu.solve(self.reduced_residual) - elif False: - # AMG for fully reduced system - tic = time.time() + elif linear_solver == "amg-potential": ml = pyamg.smoothed_aggregation_solver( - self.fully_reduced_jacobian, - # B=X.reshape( - # n * n, 1 - # ), # the representation of the near null space (this is a poor choice) - # BH=None, # the representation of the left near null space - symmetry="hermitian", # indicate that the matrix is Hermitian - # strength="evolution", # change the strength of connection - aggregate="standard", # use a standard aggregation method - smooth=( - "jacobi", - {"omega": 4.0 / 3.0, "degree": 2}, - ), # prolongation smoothing - presmoother=("block_gauss_seidel", {"sweep": "symmetric"}), - postsmoother=("block_gauss_seidel", {"sweep": "symmetric"}), - # improve_candidates=[ - # ("block_gauss_seidel", {"sweep": "symmetric", "iterations": 4}), - # None, - # ], - max_levels=4, # maximum number of levels - max_coarse=1000, # maximum number on a coarse level - # keep=False, # keep extra operators around in the hierarchy (memory) + self.fully_reduced_jacobian, **ml_options ) - print("setup ml", time.time() - tic) - if False: - print(ml) - - tol_linear_solver = self.options.get("tol_linear_solver", 1e-6) - res_history = [] + time_setup = time.time() - tic tic = time.time() update[self.fully_reduced_system_indices_full] = ml.solve( self.fully_reduced_residual, - tol=tol_linear_solver, - residuals=res_history, - ) - print("ml solve", time.time() - tic) - if False: - print( - "res: ", - len(res_history), - np.linalg.norm( - reduced_residual - - self.reduced_jacobian.dot( - update[self.reduced_system_indices] - ) - ), - ) - print("update", np.linalg.norm(update)) - - else: - # LU for fully reduced system - lu = sps.linalg.splu(self.fully_reduced_jacobian) - update[self.fully_reduced_system_indices_full] = lu.solve( - self.fully_reduced_residual + tol=tol_amg, + residuals=res_history_amg, ) # Compute flux update @@ -1250,9 +1235,19 @@ def linearization_step( residual[self.flux_indices] + self.DT.dot(update[self.reduced_system_indices]) ) + time_solve = time.time() - tic + + # Diagnostics + if linear_solver in ["amg-flux-reduced", "amg-potential"]: + if self.options.get("linear_solver_verbosity", False): + num_amg_iter = len(res_history_amg) + res_amg = res_history_amg[-1] + print(ml) + print( + f"#AMG iterations: {num_amg_iter}; Residual after AMG step: {res_amg}" + ) - toc = time.time() - time_solve = toc - tic + # Collect stats stats = [time_setup, time_solve] return update, residual, stats From 083afd04e82f9cedf0e062fdc1f6ab506e550f41 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Sat, 9 Sep 2023 15:24:30 +0200 Subject: [PATCH 028/100] MAINT: adapt bregman algorithm and use similar monitoring as for Newton --- src/darsia/measure/wasserstein.py | 103 ++++++++++++++++++++++++++---- 1 file changed, 91 insertions(+), 12 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index c85194aa..bf6374cc 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -17,6 +17,8 @@ # - improve documentation, in particular with focus on keywords # - remove plotting # - improve assembling of operators through partial assembling +# - improve stopping criteria +# - use better quadrature for l1_dissipation? class VariationalWassersteinDistance(darsia.EMD): @@ -601,7 +603,6 @@ def l1_dissipation(self, solution: np.ndarray) -> float: float: l1 dissipation potential """ - # TODO use improved quadrature? flat_flux, _, _ = self.split_solution(solution) cell_flux = self.face_to_cell(flat_flux) return np.sum(np.prod(self.voxel_size) * np.linalg.norm(cell_flux, 2, axis=-1)) @@ -636,7 +637,8 @@ def vector_face_flux_norm(self, flat_flux: np.ndarray, mode: str) -> np.ndarray: flat_flux_norm = self.cell_to_face(cell_flux_norm, mode=average_mode) elif mode == "face_arithmetic": - # Define natural vector valued flux on faces (taking averages over cells) + # Define natural vector valued flux on faces (taking arithmetic averages + # of continuous fluxes over cells evaluated at faces) tangential_flux = self.orthogonal_face_average.dot(flat_flux) # Determine the l2 norm of the fluxes on the faces, add some regularization flat_flux_norm = np.sqrt(flat_flux**2 + tangential_flux**2) @@ -1448,23 +1450,59 @@ def _solve(self, flat_mass_diff): # Keep track of how often the distance increases. num_neg_diff = 0 + # Initialize container for storing the convergence history + convergence_history = { + "distance": [], + "mass residual": [], + "force": [], + "flux increment": [], + "aux increment": [], + "force increment": [], + "distance increment": [], + "timing": [], + } + + # Print header + if self.verbose: + print( + "--- ; ", + "Bregman iteration", + "L", + "distance", + "mass conservation residual", + "increment", + "flux increment", + "distance increment", + ) + + # Initialize Bregman variables - two auxiliary variables + old_aux_variable = np.zeros(self.num_faces, dtype=float) + new_aux_variable = np.zeros(self.num_faces, dtype=float) + old_force = np.zeros(self.num_faces, dtype=float) + new_force = np.zeros(self.num_faces, dtype=float) + # Bregman iterations solution_i = np.zeros_like(rhs) + old_flat_flux_i = solution_i[:self.num_faces] for iter in range(num_iter): old_distance = self.l1_dissipation(solution_i) # 1. Solve linear system with trust in current flux. + tic = time.time() flat_flux_i, _, _ = self.split_solution(solution_i) rhs_i = rhs.copy() rhs_i[: self.num_faces] = self.L * self.mass_matrix_faces.dot(flat_flux_i) intermediate_solution_i = self.l_scheme_mixed_darcy_lu.solve(rhs_i) + time_linearization = time.time() - tic # 2. Shrink step for vectorial fluxes. To comply with the RT0 setting, the # shrinkage operation merely determines the scalar. We still aim at # following along the direction provided by the vectorial fluxes. + tic = time.time() intermediate_flat_flux_i, _, _ = self.split_solution( intermediate_solution_i ) + # Only consider normal direction (does not take into account the full flux) # new_flat_flux_i = np.sign(intermediate_flat_flux_i) * ( # np.maximum(np.abs(intermediate_flat_flux_i) - 1.0 / self.L, 0.0) # ) @@ -1475,14 +1513,18 @@ def _solve(self, flat_mass_diff): ) flat_scaling = self.cell_to_face(cell_scaling, mode="arithmetic") new_flat_flux_i = flat_scaling * intermediate_flat_flux_i + time_shrink = time.time() - tic # Apply Anderson acceleration to flux contribution (the only nonlinear part). + tic = time.time() if self.anderson is not None: flux_inc = new_flat_flux_i - flat_flux_i new_flat_flux_i = self.anderson(new_flat_flux_i, flux_inc, iter) + toc = time.time() + time_anderson = toc - tic - # Measure error in terms of the increment of the flux - flux_diff = np.linalg.norm(new_flat_flux_i - flat_flux_i, 2) + # Collect stats + stats_i = [time_linearization, time_shrink, time_anderson] # Update flux solution solution_i = intermediate_solution_i.copy() @@ -1496,8 +1538,39 @@ def _solve(self, flat_mass_diff): (rhs_i - self.broken_darcy.dot(solution_i))[self.num_faces : -1], 2 ) - # TODO include criterion build on staganation of the solution - # TODO include criterion on distance. + # Determine increments + flux_increment = new_flat_flux_i - old_flat_flux_i + aux_increment = new_aux_variable - old_aux_variable + force_increment = new_force - old_force + + # Determine force + force = np.linalg.norm(new_force, 2) + + # Compute the error: + # - residual of mass conservation equation - should be always zero if exact solver used + # - force + # - flux increment + # - aux increment + # - force increment + # - distance increment + error = [ + mass_conservation_residual, + force, + np.linalg.norm(flux_increment), + np.linalg.norm(aux_increment), + np.linalg.norm(force_increment), + abs(new_distance - old_distance), + ] + + # Update convergence history + convergence_history["distance"].append(new_distance) + convergence_history["mass residual"].append(error[0]) + convergence_history["force"].append(error[1]) + convergence_history["flux increment"].append(error[2]) + convergence_history["aux increment"].append(error[3]) + convergence_history["force increment"].append(error[4]) + convergence_history["distance increment"].append(error[5]) + convergence_history["timing"].append(stats_i) # Print status if self.verbose: @@ -1505,12 +1578,18 @@ def _solve(self, flat_mass_diff): "Bregman iteration", iter, new_distance, - old_distance - new_distance, self.L, - flux_diff, - mass_conservation_residual, + error[0], # mass conservation residual + error[1], # force + error[2], # flux increment + error[3], # aux increment + error[4], # force increment + error[5], # distance increment + stats_i, # timing ) + # TODO include criterion build on staganation of the solution + # TODO include criterion on distance. ## Check stopping criterion # TODO. What is a good stopping criterion? # if iter > 1 and (flux_diff < tol or mass_conservation_residual < tol: # break @@ -1520,7 +1599,6 @@ def _solve(self, flat_mass_diff): num_neg_diff += 1 # Increase L if stagnating of the distance increases too often. - # TODO restart anderson acceleration update_l = self.options.get("update_l", True) if update_l: tol_distance = self.options.get("tol_distance", 1e-12) @@ -1556,9 +1634,10 @@ def _solve(self, flat_mass_diff): "converged": iter < num_iter, "number iterations": iter, "distance": new_distance, - "residual mass conservation": mass_conservation_residual, - "flux increment": flux_diff, + "mass conservation residual": error[0], + "flux increment": error[2], "distance increment": abs(new_distance - old_distance), + "convergence history": convergence_history, } return new_distance, solution_i, status From d07ee0aaa299a0e3407f51b11e146a2e3843b71b Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Sun, 10 Sep 2023 01:53:50 +0200 Subject: [PATCH 029/100] MAINT: add convergence criterion for Bregman split --- src/darsia/measure/wasserstein.py | 35 ++++++++++++++++++------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index bf6374cc..fcd8ab76 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -1434,11 +1434,15 @@ def _problem_specific_setup(self, mass_diff: np.ndarray) -> None: self.l_scheme_mixed_darcy_lu = sps.linalg.splu(l_scheme_mixed_darcy) def _solve(self, flat_mass_diff): + # Solver parameters num_iter = self.options.get("num_iter", 100) - # tol = self.options.get("tol", 1e-6) # TODO make use of tol, or remove - self.L = self.options.get("L", 1.0) + tol_residual = self.options.get("tol_residual", 1e-6) + tol_increment = self.options.get("tol_increment", 1e-6) + tol_distance = self.options.get("tol_distance", 1e-6) + # Relaxation parameter + self.L = self.options.get("L", 1.0) rhs = np.concatenate( [ np.zeros(self.num_faces, dtype=float), @@ -1483,7 +1487,7 @@ def _solve(self, flat_mass_diff): # Bregman iterations solution_i = np.zeros_like(rhs) - old_flat_flux_i = solution_i[:self.num_faces] + old_flat_flux_i = solution_i[: self.num_faces] for iter in range(num_iter): old_distance = self.l1_dissipation(solution_i) @@ -1579,21 +1583,15 @@ def _solve(self, flat_mass_diff): iter, new_distance, self.L, - error[0], # mass conservation residual - error[1], # force - error[2], # flux increment - error[3], # aux increment - error[4], # force increment - error[5], # distance increment + error[0], # mass conservation residual + error[1], # force + error[2], # flux increment + error[3], # aux increment + error[4], # force increment + error[5], # distance increment stats_i, # timing ) - # TODO include criterion build on staganation of the solution - # TODO include criterion on distance. - ## Check stopping criterion # TODO. What is a good stopping criterion? - # if iter > 1 and (flux_diff < tol or mass_conservation_residual < tol: - # break - # Keep track if the distance increases. if new_distance > old_distance: num_neg_diff += 1 @@ -1629,6 +1627,13 @@ def _solve(self, flat_mass_diff): if self.L > L_max: break + # TODO include criterion build on staganation of the solution + if iter > 1 and ( + (error[0] < tol_residual and error[2] < tol_increment) + or error[5] < tol_distance + ): + break + # Define performance metric status = { "converged": iter < num_iter, From 7c572411d51df356b726a4e0b19f50afcfd8b961 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Sun, 10 Sep 2023 21:38:24 +0200 Subject: [PATCH 030/100] MAINT: reorganize code --- src/darsia/measure/wasserstein.py | 34 ++++++++++++++++++------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index fcd8ab76..c5588478 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -1433,6 +1433,25 @@ def _problem_specific_setup(self, mass_diff: np.ndarray) -> None: ) self.l_scheme_mixed_darcy_lu = sps.linalg.splu(l_scheme_mixed_darcy) + def _shrink(self, solution): + + flat_flux, _, _ = self.split_solution( + solution + ) + # Only consider normal direction (does not take into account the full flux) + # new_flat_flux_i = np.sign(intermediate_flat_flux_i) * ( + # np.maximum(np.abs(intermediate_flat_flux_i) - 1.0 / self.L, 0.0) + # ) + + # TODO use difference versions to construct the flux and the norm + cell_flux = self.face_to_cell(flat_flux) + norm = np.linalg.norm(cell_flux, 2, axis=-1) + cell_scaling = np.maximum(norm - 1 / self.L, 0) / ( + norm + self.regularization + ) + flat_scaling = self.cell_to_face(cell_scaling, mode="arithmetic") + return flat_scaling * flat_flux + def _solve(self, flat_mass_diff): # Solver parameters @@ -1503,20 +1522,7 @@ def _solve(self, flat_mass_diff): # shrinkage operation merely determines the scalar. We still aim at # following along the direction provided by the vectorial fluxes. tic = time.time() - intermediate_flat_flux_i, _, _ = self.split_solution( - intermediate_solution_i - ) - # Only consider normal direction (does not take into account the full flux) - # new_flat_flux_i = np.sign(intermediate_flat_flux_i) * ( - # np.maximum(np.abs(intermediate_flat_flux_i) - 1.0 / self.L, 0.0) - # ) - cell_intermediate_flux_i = self.face_to_cell(intermediate_flat_flux_i) - norm = np.linalg.norm(cell_intermediate_flux_i, 2, axis=-1) - cell_scaling = np.maximum(norm - 1 / self.L, 0) / ( - norm + self.regularization - ) - flat_scaling = self.cell_to_face(cell_scaling, mode="arithmetic") - new_flat_flux_i = flat_scaling * intermediate_flat_flux_i + new_flat_flux_i = self._shrink(intermediate_solution_i) time_shrink = time.time() - tic # Apply Anderson acceleration to flux contribution (the only nonlinear part). From fde2878137fb5e6f634b7616ebdd540f26ca4482 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Sun, 10 Sep 2023 23:47:46 +0200 Subject: [PATCH 031/100] MAINT: Refactor shrink --- src/darsia/measure/wasserstein.py | 56 +++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index c5588478..e32e1194 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -1433,27 +1433,46 @@ def _problem_specific_setup(self, mass_diff: np.ndarray) -> None: ) self.l_scheme_mixed_darcy_lu = sps.linalg.splu(l_scheme_mixed_darcy) - def _shrink(self, solution): + def _shrink( + self, flat_flux: np.ndarray, mode: str = "cell_arithmetic" + ) -> np.ndarray: + """Shrink operation in the split Bregman method. - flat_flux, _, _ = self.split_solution( - solution - ) - # Only consider normal direction (does not take into account the full flux) - # new_flat_flux_i = np.sign(intermediate_flat_flux_i) * ( - # np.maximum(np.abs(intermediate_flat_flux_i) - 1.0 / self.L, 0.0) - # ) + Operation on fluxes. + + Args: + flat_flux (np.ndarray): flux + mode (str, optional): mode of the shrink operation. Defaults to "cell_arithmetic". + + Returns: + np.ndarray: shrunk fluxes + + """ + if mode == "cell_arithmetic": + # Idea: Determine the shrink factor based on the cell reconstructions of the + # fluxes. Convert cell-based shrink factors to face-based shrink factors + # through arithmetic averaging. + cell_flux = self.face_to_cell(flat_flux) + norm = np.linalg.norm(cell_flux, 2, axis=-1) + cell_scaling = np.maximum(norm - 1 / self.L, 0) / ( + norm + self.regularization + ) + flat_scaling = self.cell_to_face(cell_scaling, mode="arithmetic") + + elif mode == "face_normal": + # Only consider normal direction (does not take into account the full flux) + # TODO rm. + norm = np.linalg.norm(flat_flux, 2, axis=-1) + flat_scaling = np.maximum(norm - 1 / self.L, 0) / ( + norm + self.regularization + ) + + else: + raise NotImplementedError(f"Mode {mode} not supported.") - # TODO use difference versions to construct the flux and the norm - cell_flux = self.face_to_cell(flat_flux) - norm = np.linalg.norm(cell_flux, 2, axis=-1) - cell_scaling = np.maximum(norm - 1 / self.L, 0) / ( - norm + self.regularization - ) - flat_scaling = self.cell_to_face(cell_scaling, mode="arithmetic") return flat_scaling * flat_flux def _solve(self, flat_mass_diff): - # Solver parameters num_iter = self.options.get("num_iter", 100) tol_residual = self.options.get("tol_residual", 1e-6) @@ -1522,7 +1541,10 @@ def _solve(self, flat_mass_diff): # shrinkage operation merely determines the scalar. We still aim at # following along the direction provided by the vectorial fluxes. tic = time.time() - new_flat_flux_i = self._shrink(intermediate_solution_i) + intermediate_flat_flux_i, _, _ = self.split_solution( + intermediate_solution_i + ) + new_flat_flux_i = self._shrink(intermediate_flat_flux_i) time_shrink = time.time() - tic # Apply Anderson acceleration to flux contribution (the only nonlinear part). From e691189a4bcd94fc37be5993eecb62aca37947a5 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Tue, 12 Sep 2023 18:26:45 +0200 Subject: [PATCH 032/100] BUG: Use correct mass matrix --- src/darsia/measure/wasserstein.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index e32e1194..9e467d40 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -211,7 +211,7 @@ def _setup(self) -> None: # Define sparse mass matrix on faces: flat fluxes -> flat fluxes lumping = self.options.get("lumping", True) if lumping: - self.mass_matrix_faces = sps.diags( + self.mass_matrix_faces = 0.5 * sps.diags( np.prod(self.voxel_size) * np.ones(num_faces, dtype=float) ) else: From cace02d82d3decde0fd7ce457a765c1104609333 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Tue, 12 Sep 2023 23:05:57 +0200 Subject: [PATCH 033/100] MAINT: Extend signature of l1 dissipation --- src/darsia/measure/wasserstein.py | 32 ++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index 9e467d40..58430f85 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -593,19 +593,41 @@ def transport_density(self, cell_flux: np.ndarray) -> np.ndarray: """ return np.linalg.norm(cell_flux, 2, axis=-1) - def l1_dissipation(self, solution: np.ndarray) -> float: + # TODO consider to replace transport_density with this function: + + # def compute_transport_density(self, solution: np.ndarray) -> np.ndarray: + # """Compute the transport density from the solution. + + # Args: + # solution (np.ndarray): solution + + # Returns: + # np.ndarray: transport density + + # """ + # # Compute transport density + # flat_flux, _, _ = self.split_solution(solution) + # cell_flux = self.face_to_cell(flat_flux) + # norm = np.linalg.norm(cell_flux, 2, axis=-1) + # return norm + + def l1_dissipation(self, flat_flux: np.ndarray, mode: str) -> float: """Compute the l1 dissipation potential of the solution. Args: - solution (np.ndarray): solution + flat_flux (np.ndarray): flat fluxes Returns: float: l1 dissipation potential """ - flat_flux, _, _ = self.split_solution(solution) - cell_flux = self.face_to_cell(flat_flux) - return np.sum(np.prod(self.voxel_size) * np.linalg.norm(cell_flux, 2, axis=-1)) + if mode == "cell_arithmetic": + cell_flux = self.face_to_cell(flat_flux) + cell_flux_norm = np.ravel(np.linalg.norm(cell_flux, 2, axis=-1)) + return self.mass_matrix_cells.dot(cell_flux_norm).sum() + elif mode == "face_arithmetic": + face_flux_norm = self.vector_face_flux_norm(flat_flux, "face_arithmetic") + return self.mass_matrix_faces.dot(face_flux_norm).sum() # ! ---- Lumping of effective mobility From 420fbe1b23a76fd8cb0ba79cc147deb7cbebfad2 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Tue, 12 Sep 2023 23:06:54 +0200 Subject: [PATCH 034/100] Bug: Also use a name when not storing the plots --- src/darsia/measure/wasserstein.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index 58430f85..03b35fc2 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -752,11 +752,11 @@ def _plot_solution( """ # Fetch options plot_options = self.options.get("plot_options", {}) + name = plot_options.get("name", None) # Store plot save_plot = plot_options.get("save", False) if save_plot: - name = plot_options.get("name", None) folder = plot_options.get("folder", ".") Path(folder).mkdir(parents=True, exist_ok=True) show_plot = plot_options.get("show", True) From d1b3f96e921d4b123d5a9d81aebee68de2a857cf Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Tue, 12 Sep 2023 23:08:23 +0200 Subject: [PATCH 035/100] MAINT: init implicit Bregman --- src/darsia/measure/wasserstein.py | 334 +++++++++++++++++++++--------- 1 file changed, 239 insertions(+), 95 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index 03b35fc2..8c2ee4cd 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -1345,7 +1345,8 @@ def _solve(self, flat_mass_diff: np.ndarray) -> tuple[float, np.ndarray, dict]: for iter in range(num_iter): # Keep track of old flux, and old distance old_solution_i = solution_i.copy() - old_distance = self.l1_dissipation(solution_i) + old_flux, _, _ = self.split_solution(solution_i) + old_distance = self.l1_dissipation(old_flux, "cell_arithmetic") # Newton step update_i, residual_i, stats_i = self.linearization_step( @@ -1366,7 +1367,8 @@ def _solve(self, flat_mass_diff: np.ndarray) -> tuple[float, np.ndarray, dict]: stats_i.append(time_anderson) # Update distance - new_distance = self.l1_dissipation(solution_i) + new_flux, _, _ = self.split_solution(solution_i) + new_distance = self.l1_dissipation(new_flux, "cell_arithmetic") # Compute the error: # - full residual @@ -1445,18 +1447,30 @@ class WassersteinDistanceBregman(VariationalWassersteinDistance): def _problem_specific_setup(self, mass_diff: np.ndarray) -> None: super()._problem_specific_setup(mass_diff) self.L = self.options.get("L", 1.0) - l_scheme_mixed_darcy = sps.bmat( - [ - [self.L * self.mass_matrix_faces, -self.div.T, None], - [self.div, None, -self.potential_constraint.T], - [None, self.potential_constraint, None], - ], - format="csc", - ) + if False: + # Use for explicit Bregman + l_scheme_mixed_darcy = sps.bmat( + [ + [self.L * self.mass_matrix_faces, -self.div.T, None], + [self.div, None, -self.potential_constraint.T], + [None, self.potential_constraint, None], + ], + format="csc", + ) + elif True: + # Only use for implicit Bregman + l_scheme_mixed_darcy = sps.bmat( + [ + [1.5 * self.L * self.mass_matrix_faces, -self.div.T, None], + [self.div, None, -self.potential_constraint.T], + [None, self.potential_constraint, None], + ], + format="csc", + ) self.l_scheme_mixed_darcy_lu = sps.linalg.splu(l_scheme_mixed_darcy) def _shrink( - self, flat_flux: np.ndarray, mode: str = "cell_arithmetic" + self, flat_flux: np.ndarray, shrink_factor: float, mode: str = "cell_arithmetic" ) -> np.ndarray: """Shrink operation in the split Bregman method. @@ -1464,6 +1478,7 @@ def _shrink( Args: flat_flux (np.ndarray): flux + shrink_factor (float): shrink factor mode (str, optional): mode of the shrink operation. Defaults to "cell_arithmetic". Returns: @@ -1476,16 +1491,26 @@ def _shrink( # through arithmetic averaging. cell_flux = self.face_to_cell(flat_flux) norm = np.linalg.norm(cell_flux, 2, axis=-1) - cell_scaling = np.maximum(norm - 1 / self.L, 0) / ( + cell_scaling = np.maximum(norm - shrink_factor, 0) / ( norm + self.regularization ) flat_scaling = self.cell_to_face(cell_scaling, mode="arithmetic") + elif mode == "face_arithmetic": + # Define natural vector valued flux on faces (taking arithmetic averages + # of continuous fluxes over cells evaluated at faces) + tangential_flux = self.orthogonal_face_average.dot(flat_flux) + # Determine the l2 norm of the fluxes on the faces, add some regularization + norm = np.sqrt(flat_flux**2 + tangential_flux**2) + flat_scaling = np.maximum(norm - shrink_factor, 0) / ( + norm + self.regularization + ) + elif mode == "face_normal": # Only consider normal direction (does not take into account the full flux) # TODO rm. norm = np.linalg.norm(flat_flux, 2, axis=-1) - flat_scaling = np.maximum(norm - 1 / self.L, 0) / ( + flat_scaling = np.maximum(norm - shrink_factor, 0) / ( norm + self.regularization ) @@ -1499,7 +1524,7 @@ def _solve(self, flat_mass_diff): num_iter = self.options.get("num_iter", 100) tol_residual = self.options.get("tol_residual", 1e-6) tol_increment = self.options.get("tol_increment", 1e-6) - tol_distance = self.options.get("tol_distance", 1e-6) + # tol_distance = self.options.get("tol_distance", 1e-6) # Relaxation parameter self.L = self.options.get("L", 1.0) @@ -1534,67 +1559,176 @@ def _solve(self, flat_mass_diff): "L", "distance", "mass conservation residual", - "increment", - "flux increment", + "[flux, aux, force] increment", "distance increment", ) - # Initialize Bregman variables - two auxiliary variables - old_aux_variable = np.zeros(self.num_faces, dtype=float) - new_aux_variable = np.zeros(self.num_faces, dtype=float) - old_force = np.zeros(self.num_faces, dtype=float) - new_force = np.zeros(self.num_faces, dtype=float) + # Initialize Bregman variables and flux with Darcy flow + shrink_mode = "face_arithmetic" + dissipation_mode = "cell_arithmetic" + if False: + shrink_factor = 1.0 / self.L + else: + shrink_factor = 2.0 / (3.0 * self.L) + solution_i = self.l_scheme_mixed_darcy_lu.solve(rhs) + old_flux, _, _ = self.split_solution(solution_i) + old_aux_flux = self._shrink(old_flux, shrink_factor, shrink_mode) + old_force = old_flux - old_aux_flux + very_old_force = np.zeros_like(old_force, dtype=float) + old_distance = self.l1_dissipation(old_flux, dissipation_mode) - # Bregman iterations - solution_i = np.zeros_like(rhs) - old_flat_flux_i = solution_i[: self.num_faces] for iter in range(num_iter): - old_distance = self.l1_dissipation(solution_i) + # # 1. Solve linear system with trust in current flux. + # tic = time.time() + # flat_flux_i, _, _ = self.split_solution(solution_i) + # rhs_i = rhs.copy() + # rhs_i[: self.num_faces] = self.L * self.mass_matrix_faces.dot(flat_flux_i) + # intermediate_solution_i = self.l_scheme_mixed_darcy_lu.solve(rhs_i) + # time_linearization = time.time() - tic + + # # 2. Shrink step for vectorial fluxes. To comply with the RT0 setting, the + # # shrinkage operation merely determines the scalar. We still aim at + # # following along the direction provided by the vectorial fluxes. + # tic = time.time() + # intermediate_flat_flux_i, _, _ = self.split_solution( + # intermediate_solution_i + # ) + # new_flat_flux_i = self._shrink(intermediate_flat_flux_i, shrink_mode) + # time_shrink = time.time() - tic + + if False: + # (Explicit) Bregman method + + # 1. Make relaxation step (solve quadratic optimization problem) + tic = time.time() + rhs_i = rhs.copy() + rhs_i[: self.num_faces] = self.L * self.mass_matrix_faces.dot( + old_aux_flux - old_force + ) + new_flux, _, _ = self.split_solution( + self.l_scheme_mixed_darcy_lu.solve(rhs_i) + ) + time_linearization = time.time() - tic - # 1. Solve linear system with trust in current flux. - tic = time.time() - flat_flux_i, _, _ = self.split_solution(solution_i) - rhs_i = rhs.copy() - rhs_i[: self.num_faces] = self.L * self.mass_matrix_faces.dot(flat_flux_i) - intermediate_solution_i = self.l_scheme_mixed_darcy_lu.solve(rhs_i) - time_linearization = time.time() - tic - - # 2. Shrink step for vectorial fluxes. To comply with the RT0 setting, the - # shrinkage operation merely determines the scalar. We still aim at - # following along the direction provided by the vectorial fluxes. - tic = time.time() - intermediate_flat_flux_i, _, _ = self.split_solution( - intermediate_solution_i - ) - new_flat_flux_i = self._shrink(intermediate_flat_flux_i) - time_shrink = time.time() - tic + # 2. Shrink step for vectorial fluxes. To comply with the RT0 setting, the + # shrinkage operation merely determines the scalar. We still aim at + # following along the direction provided by the vectorial fluxes. + tic = time.time() + new_aux_flux = self._shrink( + new_flux + old_force, shrink_factor, shrink_mode + ) + time_shrink = time.time() - tic - # Apply Anderson acceleration to flux contribution (the only nonlinear part). - tic = time.time() - if self.anderson is not None: - flux_inc = new_flat_flux_i - flat_flux_i - new_flat_flux_i = self.anderson(new_flat_flux_i, flux_inc, iter) - toc = time.time() - time_anderson = toc - tic + # 3. Update force + new_force = old_force + new_flux - new_aux_flux + + # Apply Anderson acceleration to flux contribution (the only nonlinear part). + tic = time.time() + if self.anderson is not None: + aux_inc = new_aux_flux - old_aux_flux + force_inc = new_force - old_force + inc = np.concatenate([aux_inc, force_inc]) + iteration = np.concatenate([new_aux_flux, new_force]) + new_iteration = self.anderson(iteration, inc, iter) + new_aux_flux = new_iteration[: self.num_faces] + new_force = new_iteration[self.num_faces :] + + toc = time.time() + time_anderson = toc - tic + + elif True: + # Implicit Bregman method + + # 1. Make relaxation step (solve quadratic optimization problem) + tic = time.time() + rhs_i = rhs.copy() + rhs_i[: self.num_faces] = ( + 1.5 + * self.L + * self.mass_matrix_faces.dot(old_aux_flux - very_old_force) + ) + new_flux, _, _ = self.split_solution( + self.l_scheme_mixed_darcy_lu.solve(rhs_i) + ) + time_linearization = time.time() - tic + + # 2. Shrink step for vectorial fluxes. To comply with the RT0 setting, the + # shrinkage operation merely determines the scalar. We still aim at + # following along the direction provided by the vectorial fluxes. + tic = time.time() + new_aux_flux = self._shrink( + new_flux + very_old_force, shrink_factor, shrink_mode + ) + time_shrink = time.time() - tic + + # 3. Update force + new_force = old_force + 1.0 / 3.0 * (new_flux - new_aux_flux) + + # Apply Anderson acceleration to flux contribution (the only nonlinear part). + tic = time.time() + if self.anderson is not None: + aux_inc = new_aux_flux - old_aux_flux + force_inc = new_force - old_force + inc = np.concatenate([aux_inc, force_inc]) + iteration = np.concatenate([new_aux_flux, new_force]) + new_iteration = self.anderson(iteration, inc, iter) + new_aux_flux = new_iteration[: self.num_faces] + new_force = new_iteration[self.num_faces :] + + toc = time.time() + time_anderson = toc - tic + elif False: + # 1. Shrink step for vectorial fluxes. To comply with the RT0 setting, the + # shrinkage operation merely determines the scalar. We still aim at + # following along the direction provided by the vectorial fluxes. + tic = time.time() + new_aux_flux = self._shrink( + old_flux + old_force, shrink_factor, shrink_mode + ) + time_shrink = time.time() - tic + + # 2. Update force + new_force = old_force + old_flux - new_aux_flux + + # 3. Solve linear system with trust in current flux. + tic = time.time() + rhs_i = rhs.copy() + rhs_i[: self.num_faces] = self.L * self.mass_matrix_faces.dot( + new_aux_flux - new_force + ) + new_flux, _, _ = self.split_solution( + self.l_scheme_mixed_darcy_lu.solve(rhs_i) + ) + time_linearization = time.time() - tic + + # Apply Anderson acceleration to flux contribution (the only nonlinear part). + tic = time.time() + if self.anderson is not None: + flux_inc = new_flux - old_flux + force_inc = new_force - old_force + inc = np.concatenate([flux_inc, force_inc]) + iteration = np.concatenate([new_flux, new_force]) + # new_flux = self.anderson(new_flux, flux_inc, iter) + new_iteration = self.anderson(iteration, inc, iter) + new_flux = new_iteration[: self.num_faces] + new_force = new_iteration[self.num_faces :] + toc = time.time() + time_anderson = toc - tic # Collect stats stats_i = [time_linearization, time_shrink, time_anderson] - # Update flux solution - solution_i = intermediate_solution_i.copy() - solution_i[: self.num_faces] = new_flat_flux_i - # Update distance - new_distance = self.l1_dissipation(solution_i) + new_distance = self.l1_dissipation(new_flux, dissipation_mode) # Determine the error in the mass conservation equation mass_conservation_residual = np.linalg.norm( - (rhs_i - self.broken_darcy.dot(solution_i))[self.num_faces : -1], 2 + self.div.dot(new_flux) - rhs[self.num_faces : -1], 2 ) # Determine increments - flux_increment = new_flat_flux_i - old_flat_flux_i - aux_increment = new_aux_variable - old_aux_variable + flux_increment = new_flux - old_flux + aux_increment = new_aux_flux - old_aux_flux force_increment = new_force - old_force # Determine force @@ -1634,12 +1768,13 @@ def _solve(self, flat_mass_diff): new_distance, self.L, error[0], # mass conservation residual - error[1], # force - error[2], # flux increment - error[3], # aux increment - error[4], # force increment + [ + error[2], # flux increment + error[3], # aux increment + error[4], # force increment + ], error[5], # distance increment - stats_i, # timing + # stats_i, # timings ) # Keep track if the distance increases. @@ -1647,43 +1782,52 @@ def _solve(self, flat_mass_diff): num_neg_diff += 1 # Increase L if stagnating of the distance increases too often. - update_l = self.options.get("update_l", True) - if update_l: - tol_distance = self.options.get("tol_distance", 1e-12) - max_iter_increase_diff = self.options.get("max_iter_increase_diff", 20) - l_factor = self.options.get("l_factor", 2) - if ( - abs(new_distance - old_distance) < tol_distance - or num_neg_diff > max_iter_increase_diff - ): - # Update L - self.L = self.L * l_factor - - # Update linear system - l_scheme_mixed_darcy = sps.bmat( - [ - [self.L * self.mass_matrix_faces, -self.div.T, None], - [self.div, None, -self.potential_constraint.T], - [None, self.potential_constraint, None], - ], - format="csc", - ) - self.l_scheme_mixed_darcy_lu = sps.linalg.splu(l_scheme_mixed_darcy) - - # Reset stagnation counter - num_neg_diff = 0 - - L_max = self.options.get("L_max", 1e8) - if self.L > L_max: - break + update_l = self.options.get("update_l", False) + # if update_l: + # tol_distance = self.options.get("tol_distance", 1e-12) + # max_iter_increase_diff = self.options.get("max_iter_increase_diff", 20) + # l_factor = self.options.get("l_factor", 2) + # if ( + # abs(new_distance - old_distance) < tol_distance + # or num_neg_diff > max_iter_increase_diff + # ): + # # Update L + # self.L = self.L * l_factor + # + # # Update linear system + # l_scheme_mixed_darcy = sps.bmat( + # [ + # [self.L * self.mass_matrix_faces, -self.div.T, None], + # [self.div, None, -self.potential_constraint.T], + # [None, self.potential_constraint, None], + # ], + # format="csc", + # ) + # self.l_scheme_mixed_darcy_lu = sps.linalg.splu(l_scheme_mixed_darcy) + # + # # Reset stagnation counter + # num_neg_diff = 0 + # + # L_max = self.options.get("L_max", 1e8) + # if self.L > L_max: + # break # TODO include criterion build on staganation of the solution - if iter > 1 and ( - (error[0] < tol_residual and error[2] < tol_increment) - or error[5] < tol_distance - ): + if iter > 1 and ((error[0] < tol_residual and error[4] < tol_increment)): break + # Update Bregman variables + old_flux = new_flux.copy() + old_aux_flux = new_aux_flux.copy() + very_old_force = old_force.copy() + old_force = new_force.copy() + old_distance = new_distance + + # TODO solve for potential and multiplier + solution_i = np.zeros_like(rhs) + solution_i[: self.num_faces] = new_flux.copy() + # TODO continue + # Define performance metric status = { "converged": iter < num_iter, From c11d8b8aaa8d4ec8d07066583802ced0b38f8c73 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Wed, 13 Sep 2023 08:32:52 +0200 Subject: [PATCH 036/100] MAINT: Add flag for switching between implicit and explicit Bregman --- src/darsia/measure/wasserstein.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index 8c2ee4cd..4bc5606f 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -1447,7 +1447,8 @@ class WassersteinDistanceBregman(VariationalWassersteinDistance): def _problem_specific_setup(self, mass_diff: np.ndarray) -> None: super()._problem_specific_setup(mass_diff) self.L = self.options.get("L", 1.0) - if False: + self.explicit = False + if self.explicit: # Use for explicit Bregman l_scheme_mixed_darcy = sps.bmat( [ @@ -1457,7 +1458,7 @@ def _problem_specific_setup(self, mass_diff: np.ndarray) -> None: ], format="csc", ) - elif True: + elif not(self.explicit): # Only use for implicit Bregman l_scheme_mixed_darcy = sps.bmat( [ @@ -1566,7 +1567,7 @@ def _solve(self, flat_mass_diff): # Initialize Bregman variables and flux with Darcy flow shrink_mode = "face_arithmetic" dissipation_mode = "cell_arithmetic" - if False: + if self.explicit: shrink_factor = 1.0 / self.L else: shrink_factor = 2.0 / (3.0 * self.L) @@ -1596,7 +1597,7 @@ def _solve(self, flat_mass_diff): # new_flat_flux_i = self._shrink(intermediate_flat_flux_i, shrink_mode) # time_shrink = time.time() - tic - if False: + if True and self.explicit: # (Explicit) Bregman method # 1. Make relaxation step (solve quadratic optimization problem) @@ -1636,7 +1637,7 @@ def _solve(self, flat_mass_diff): toc = time.time() time_anderson = toc - tic - elif True: + elif True and not (self.explicit): # Implicit Bregman method # 1. Make relaxation step (solve quadratic optimization problem) From 24dd72c8cdf6b6bd6a506204c7f8fd1f10f20126 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Thu, 14 Sep 2023 00:38:36 +0200 Subject: [PATCH 037/100] MAINT: backup of all old code snippets --- src/darsia/measure/wasserstein.py | 178 +++++++++++++++++++++++------- 1 file changed, 136 insertions(+), 42 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index 4bc5606f..95b89557 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -1458,7 +1458,7 @@ def _problem_specific_setup(self, mass_diff: np.ndarray) -> None: ], format="csc", ) - elif not(self.explicit): + elif not (self.explicit): # Only use for implicit Bregman l_scheme_mixed_darcy = sps.bmat( [ @@ -1637,48 +1637,142 @@ def _solve(self, flat_mass_diff): toc = time.time() time_anderson = toc - tic - elif True and not (self.explicit): - # Implicit Bregman method - - # 1. Make relaxation step (solve quadratic optimization problem) - tic = time.time() - rhs_i = rhs.copy() - rhs_i[: self.num_faces] = ( - 1.5 - * self.L - * self.mass_matrix_faces.dot(old_aux_flux - very_old_force) - ) - new_flux, _, _ = self.split_solution( - self.l_scheme_mixed_darcy_lu.solve(rhs_i) - ) - time_linearization = time.time() - tic - - # 2. Shrink step for vectorial fluxes. To comply with the RT0 setting, the - # shrinkage operation merely determines the scalar. We still aim at - # following along the direction provided by the vectorial fluxes. - tic = time.time() - new_aux_flux = self._shrink( - new_flux + very_old_force, shrink_factor, shrink_mode - ) - time_shrink = time.time() - tic - - # 3. Update force - new_force = old_force + 1.0 / 3.0 * (new_flux - new_aux_flux) - - # Apply Anderson acceleration to flux contribution (the only nonlinear part). - tic = time.time() - if self.anderson is not None: - aux_inc = new_aux_flux - old_aux_flux - force_inc = new_force - old_force - inc = np.concatenate([aux_inc, force_inc]) - iteration = np.concatenate([new_aux_flux, new_force]) - new_iteration = self.anderson(iteration, inc, iter) - new_aux_flux = new_iteration[: self.num_faces] - new_force = new_iteration[self.num_faces :] - - toc = time.time() - time_anderson = toc - tic + # elif False and self.explicit: + # # Enhanced Bregman method + # + # # old_flux_norm = np.maximum( + # # self.vector_face_flux_norm(old_flux, mode="face_arithmetic"), + # # self.regularization, + # # ) + # # enhancement = shrink_factor * old_flux / old_flux_norm + # # old_pressure_gradient = -self.div.T.dot(old_pressure) + # # old_aux_flux_norm = np.maximum( + # # self.vector_face_flux_norm(old_aux_flux, mode="face_arithmetic"), + # # self.regularization, + # # ) + # # enhancement += shrink_factor * ( + # # old_aux_flux / old_aux_flux_norm + old_pressure_gradient + # # ) + # + # # 1. Make relaxation step (solve quadratic optimization problem) + # tic = time.time() + # rhs_i = rhs.copy() + # rhs_i[: self.num_faces] = self.L * self.mass_matrix_faces.dot( + # old_aux_flux - old_force + # ) + # new_flux, _, _ = self.split_solution( + # self.l_scheme_mixed_darcy_lu.solve(rhs_i) + # ) + # time_linearization = time.time() - tic + # + # ## Solve Newton with trust in new flux to determine the corresponindg + # ## pressure + # # new_flux_norm = np.maximum( + # # self.vector_face_flux_norm(new_flux, mode="face_arithmetic"), + # # self.regularization, + # # ) + # # regularized_scaling = sps.diags( + # # np.maximum(0.01, 1.0 / new_flux_norm), dtype=float + # # ) + # # aux_rhs = rhs.copy() + # # aux_jacobian = sps.bmat( + # # [ + # # [ + # # regularized_scaling * self.mass_matrix_faces, + # # -self.div.T, + # # None, + # # ], + # # [self.div, None, -self.potential_constraint.T], + # # [None, self.potential_constraint, None], + # # ], + # # format="csc", + # # ) + # # aux_jacobian_lu = sps.linalg.splu(aux_jacobian) + # # aux_update = aux_jacobian_lu.solve(aux_rhs) + # # aux_flux, aux_pressure, _ = self.split_solution(aux_update) + # # print(np.linalg.norm(aux_flux - new_flux)) + # # print(np.linalg.norm(aux_flux)) + # # assert False + # + # # 2. Shrink step for vectorial fluxes. To comply with the RT0 setting, the + # # shrinkage operation merely determines the scalar. We still aim at + # # following along the direction provided by the vectorial fluxes. + # tic = time.time() + # new_aux_flux = self._shrink( + # new_flux + old_force - enhancement, shrink_factor, shrink_mode + # ) + # time_shrink = time.time() - tic + # + # # 3. Update force + # new_force = old_force + new_flux - new_aux_flux + # old_pressure_gradient = -self.div.T.dot(old_pressure) + # old_aux_flux_norm = np.maximum( + # self.vector_face_flux_norm(old_aux_flux, mode="face_arithmetic"), + # self.regularization, + # ) + # enhancement += shrink_factor * ( + # old_aux_flux / old_aux_flux_norm + old_pressure_gradient + # ) + # + # # Apply Anderson acceleration to flux contribution (the only nonlinear part). + # tic = time.time() + # if self.anderson is not None: + # aux_inc = new_aux_flux - old_aux_flux + # force_inc = new_force - old_force + # inc = np.concatenate([aux_inc, force_inc]) + # iteration = np.concatenate([new_aux_flux, new_force]) + # new_iteration = self.anderson(iteration, inc, iter) + # new_aux_flux = new_iteration[: self.num_faces] + # new_force = new_iteration[self.num_faces :] + # + # toc = time.time() + # time_anderson = toc - tic + # + # elif False and not (self.explicit): + # # Implicit Bregman method + # + # # 1. Make relaxation step (solve quadratic optimization problem) + # tic = time.time() + # rhs_i = rhs.copy() + # rhs_i[: self.num_faces] = ( + # 1.5 + # * self.L + # * self.mass_matrix_faces.dot(old_aux_flux - very_old_force) + # ) + # new_flux, _, _ = self.split_solution( + # self.l_scheme_mixed_darcy_lu.solve(rhs_i) + # ) + # time_linearization = time.time() - tic + # + # # 2. Shrink step for vectorial fluxes. To comply with the RT0 setting, the + # # shrinkage operation merely determines the scalar. We still aim at + # # following along the direction provided by the vectorial fluxes. + # tic = time.time() + # new_aux_flux = self._shrink( + # new_flux + very_old_force, shrink_factor, shrink_mode + # ) + # time_shrink = time.time() - tic + # + # # 3. Update force + # new_force = old_force + 1.0 / 3.0 * (new_flux - new_aux_flux) + # + # # Apply Anderson acceleration to flux contribution (the only nonlinear part). + # tic = time.time() + # if self.anderson is not None: + # aux_inc = new_aux_flux - old_aux_flux + # # force_inc = new_force - old_force + # force_inc = old_force - very_old_force + # inc = np.concatenate([aux_inc, force_inc]) + # iteration = np.concatenate([new_aux_flux, old_force]) + # new_iteration = self.anderson(iteration, inc, iter) + # new_aux_flux = new_iteration[: self.num_faces] + # # old_force = new_iteration[self.num_faces :] + # + # toc = time.time() + # time_anderson = toc - tic elif False: + # Reordered split Bregman method + # 1. Shrink step for vectorial fluxes. To comply with the RT0 setting, the # shrinkage operation merely determines the scalar. We still aim at # following along the direction provided by the vectorial fluxes. From 50f7d8dd4bcc674d95450cf624e4b291adf91670 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Thu, 14 Sep 2023 00:41:30 +0200 Subject: [PATCH 038/100] MAINT: remove attempts to improve Bregman --- src/darsia/measure/wasserstein.py | 191 ++---------------------------- 1 file changed, 11 insertions(+), 180 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index 95b89557..6057b765 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -1447,27 +1447,14 @@ class WassersteinDistanceBregman(VariationalWassersteinDistance): def _problem_specific_setup(self, mass_diff: np.ndarray) -> None: super()._problem_specific_setup(mass_diff) self.L = self.options.get("L", 1.0) - self.explicit = False - if self.explicit: - # Use for explicit Bregman - l_scheme_mixed_darcy = sps.bmat( - [ - [self.L * self.mass_matrix_faces, -self.div.T, None], - [self.div, None, -self.potential_constraint.T], - [None, self.potential_constraint, None], - ], - format="csc", - ) - elif not (self.explicit): - # Only use for implicit Bregman - l_scheme_mixed_darcy = sps.bmat( - [ - [1.5 * self.L * self.mass_matrix_faces, -self.div.T, None], - [self.div, None, -self.potential_constraint.T], - [None, self.potential_constraint, None], - ], - format="csc", - ) + l_scheme_mixed_darcy = sps.bmat( + [ + [self.L * self.mass_matrix_faces, -self.div.T, None], + [self.div, None, -self.potential_constraint.T], + [None, self.potential_constraint, None], + ], + format="csc", + ) self.l_scheme_mixed_darcy_lu = sps.linalg.splu(l_scheme_mixed_darcy) def _shrink( @@ -1567,38 +1554,16 @@ def _solve(self, flat_mass_diff): # Initialize Bregman variables and flux with Darcy flow shrink_mode = "face_arithmetic" dissipation_mode = "cell_arithmetic" - if self.explicit: - shrink_factor = 1.0 / self.L - else: - shrink_factor = 2.0 / (3.0 * self.L) + shrink_factor = 1.0 / self.L solution_i = self.l_scheme_mixed_darcy_lu.solve(rhs) old_flux, _, _ = self.split_solution(solution_i) old_aux_flux = self._shrink(old_flux, shrink_factor, shrink_mode) old_force = old_flux - old_aux_flux - very_old_force = np.zeros_like(old_force, dtype=float) old_distance = self.l1_dissipation(old_flux, dissipation_mode) for iter in range(num_iter): - # # 1. Solve linear system with trust in current flux. - # tic = time.time() - # flat_flux_i, _, _ = self.split_solution(solution_i) - # rhs_i = rhs.copy() - # rhs_i[: self.num_faces] = self.L * self.mass_matrix_faces.dot(flat_flux_i) - # intermediate_solution_i = self.l_scheme_mixed_darcy_lu.solve(rhs_i) - # time_linearization = time.time() - tic - - # # 2. Shrink step for vectorial fluxes. To comply with the RT0 setting, the - # # shrinkage operation merely determines the scalar. We still aim at - # # following along the direction provided by the vectorial fluxes. - # tic = time.time() - # intermediate_flat_flux_i, _, _ = self.split_solution( - # intermediate_solution_i - # ) - # new_flat_flux_i = self._shrink(intermediate_flat_flux_i, shrink_mode) - # time_shrink = time.time() - tic - - if True and self.explicit: - # (Explicit) Bregman method + if True: + # std split Bregman method # 1. Make relaxation step (solve quadratic optimization problem) tic = time.time() @@ -1637,139 +1602,6 @@ def _solve(self, flat_mass_diff): toc = time.time() time_anderson = toc - tic - # elif False and self.explicit: - # # Enhanced Bregman method - # - # # old_flux_norm = np.maximum( - # # self.vector_face_flux_norm(old_flux, mode="face_arithmetic"), - # # self.regularization, - # # ) - # # enhancement = shrink_factor * old_flux / old_flux_norm - # # old_pressure_gradient = -self.div.T.dot(old_pressure) - # # old_aux_flux_norm = np.maximum( - # # self.vector_face_flux_norm(old_aux_flux, mode="face_arithmetic"), - # # self.regularization, - # # ) - # # enhancement += shrink_factor * ( - # # old_aux_flux / old_aux_flux_norm + old_pressure_gradient - # # ) - # - # # 1. Make relaxation step (solve quadratic optimization problem) - # tic = time.time() - # rhs_i = rhs.copy() - # rhs_i[: self.num_faces] = self.L * self.mass_matrix_faces.dot( - # old_aux_flux - old_force - # ) - # new_flux, _, _ = self.split_solution( - # self.l_scheme_mixed_darcy_lu.solve(rhs_i) - # ) - # time_linearization = time.time() - tic - # - # ## Solve Newton with trust in new flux to determine the corresponindg - # ## pressure - # # new_flux_norm = np.maximum( - # # self.vector_face_flux_norm(new_flux, mode="face_arithmetic"), - # # self.regularization, - # # ) - # # regularized_scaling = sps.diags( - # # np.maximum(0.01, 1.0 / new_flux_norm), dtype=float - # # ) - # # aux_rhs = rhs.copy() - # # aux_jacobian = sps.bmat( - # # [ - # # [ - # # regularized_scaling * self.mass_matrix_faces, - # # -self.div.T, - # # None, - # # ], - # # [self.div, None, -self.potential_constraint.T], - # # [None, self.potential_constraint, None], - # # ], - # # format="csc", - # # ) - # # aux_jacobian_lu = sps.linalg.splu(aux_jacobian) - # # aux_update = aux_jacobian_lu.solve(aux_rhs) - # # aux_flux, aux_pressure, _ = self.split_solution(aux_update) - # # print(np.linalg.norm(aux_flux - new_flux)) - # # print(np.linalg.norm(aux_flux)) - # # assert False - # - # # 2. Shrink step for vectorial fluxes. To comply with the RT0 setting, the - # # shrinkage operation merely determines the scalar. We still aim at - # # following along the direction provided by the vectorial fluxes. - # tic = time.time() - # new_aux_flux = self._shrink( - # new_flux + old_force - enhancement, shrink_factor, shrink_mode - # ) - # time_shrink = time.time() - tic - # - # # 3. Update force - # new_force = old_force + new_flux - new_aux_flux - # old_pressure_gradient = -self.div.T.dot(old_pressure) - # old_aux_flux_norm = np.maximum( - # self.vector_face_flux_norm(old_aux_flux, mode="face_arithmetic"), - # self.regularization, - # ) - # enhancement += shrink_factor * ( - # old_aux_flux / old_aux_flux_norm + old_pressure_gradient - # ) - # - # # Apply Anderson acceleration to flux contribution (the only nonlinear part). - # tic = time.time() - # if self.anderson is not None: - # aux_inc = new_aux_flux - old_aux_flux - # force_inc = new_force - old_force - # inc = np.concatenate([aux_inc, force_inc]) - # iteration = np.concatenate([new_aux_flux, new_force]) - # new_iteration = self.anderson(iteration, inc, iter) - # new_aux_flux = new_iteration[: self.num_faces] - # new_force = new_iteration[self.num_faces :] - # - # toc = time.time() - # time_anderson = toc - tic - # - # elif False and not (self.explicit): - # # Implicit Bregman method - # - # # 1. Make relaxation step (solve quadratic optimization problem) - # tic = time.time() - # rhs_i = rhs.copy() - # rhs_i[: self.num_faces] = ( - # 1.5 - # * self.L - # * self.mass_matrix_faces.dot(old_aux_flux - very_old_force) - # ) - # new_flux, _, _ = self.split_solution( - # self.l_scheme_mixed_darcy_lu.solve(rhs_i) - # ) - # time_linearization = time.time() - tic - # - # # 2. Shrink step for vectorial fluxes. To comply with the RT0 setting, the - # # shrinkage operation merely determines the scalar. We still aim at - # # following along the direction provided by the vectorial fluxes. - # tic = time.time() - # new_aux_flux = self._shrink( - # new_flux + very_old_force, shrink_factor, shrink_mode - # ) - # time_shrink = time.time() - tic - # - # # 3. Update force - # new_force = old_force + 1.0 / 3.0 * (new_flux - new_aux_flux) - # - # # Apply Anderson acceleration to flux contribution (the only nonlinear part). - # tic = time.time() - # if self.anderson is not None: - # aux_inc = new_aux_flux - old_aux_flux - # # force_inc = new_force - old_force - # force_inc = old_force - very_old_force - # inc = np.concatenate([aux_inc, force_inc]) - # iteration = np.concatenate([new_aux_flux, old_force]) - # new_iteration = self.anderson(iteration, inc, iter) - # new_aux_flux = new_iteration[: self.num_faces] - # # old_force = new_iteration[self.num_faces :] - # - # toc = time.time() - # time_anderson = toc - tic elif False: # Reordered split Bregman method @@ -1914,7 +1746,6 @@ def _solve(self, flat_mass_diff): # Update Bregman variables old_flux = new_flux.copy() old_aux_flux = new_aux_flux.copy() - very_old_force = old_force.copy() old_force = new_force.copy() old_distance = new_distance From 9d432018994cb1b033818d84301630e983891399 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Thu, 14 Sep 2023 11:06:29 +0200 Subject: [PATCH 039/100] ENH: Add new Bregman type split, with updated weights and shrink factor. --- src/darsia/measure/wasserstein.py | 69 ++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index 6057b765..96c4c8d3 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -1458,7 +1458,10 @@ def _problem_specific_setup(self, mass_diff: np.ndarray) -> None: self.l_scheme_mixed_darcy_lu = sps.linalg.splu(l_scheme_mixed_darcy) def _shrink( - self, flat_flux: np.ndarray, shrink_factor: float, mode: str = "cell_arithmetic" + self, + flat_flux: np.ndarray, + shrink_factor: Union[float, np.ndarray], + mode: str = "cell_arithmetic", ) -> np.ndarray: """Shrink operation in the split Bregman method. @@ -1466,7 +1469,7 @@ def _shrink( Args: flat_flux (np.ndarray): flux - shrink_factor (float): shrink factor + shrink_factor (float or np.ndarray): shrink factor mode (str, optional): mode of the shrink operation. Defaults to "cell_arithmetic". Returns: @@ -1554,6 +1557,7 @@ def _solve(self, flat_mass_diff): # Initialize Bregman variables and flux with Darcy flow shrink_mode = "face_arithmetic" dissipation_mode = "cell_arithmetic" + weight = self.L shrink_factor = 1.0 / self.L solution_i = self.l_scheme_mixed_darcy_lu.solve(rhs) old_flux, _, _ = self.split_solution(solution_i) @@ -1642,6 +1646,67 @@ def _solve(self, flat_mass_diff): toc = time.time() time_anderson = toc - tic + elif True: + # Bregman split with updated weight + update_cond = self.options.get("bregman_update_cond", lambda iter: True) + if update_cond(iter): + # Update weight as the inverse of the norm of the flux + old_flux_norm = np.maximum( + self.vector_face_flux_norm(old_flux, "face_arithmetic"), + self.regularization, + ) + old_flux_norm_inv = 1.0 / old_flux_norm + weight = sps.diags(old_flux_norm_inv) + shrink_factor = old_flux_norm + + # Redefine Darcy system + l_scheme_mixed_darcy = sps.bmat( + [ + [weight * self.mass_matrix_faces, -self.div.T, None], + [self.div, None, -self.potential_constraint.T], + [None, self.potential_constraint, None], + ], + format="csc", + ) + self.l_scheme_mixed_darcy_lu = sps.linalg.splu(l_scheme_mixed_darcy) + + # 1. Make relaxation step (solve quadratic optimization problem) + tic = time.time() + rhs_i = rhs.copy() + rhs_i[: self.num_faces] = weight * self.mass_matrix_faces.dot( + old_aux_flux - old_force + ) + new_flux, _, _ = self.split_solution( + self.l_scheme_mixed_darcy_lu.solve(rhs_i) + ) + time_linearization = time.time() - tic + + # 2. Shrink step for vectorial fluxes. To comply with the RT0 setting, the + # shrinkage operation merely determines the scalar. We still aim at + # following along the direction provided by the vectorial fluxes. + tic = time.time() + new_aux_flux = self._shrink( + new_flux + old_force, shrink_factor, shrink_mode + ) + time_shrink = time.time() - tic + + # 3. Update force + new_force = old_force + new_flux - new_aux_flux + + # Apply Anderson acceleration to flux contribution (the only nonlinear part). + tic = time.time() + if self.anderson is not None: + aux_inc = new_aux_flux - old_aux_flux + force_inc = new_force - old_force + inc = np.concatenate([aux_inc, force_inc]) + iteration = np.concatenate([new_aux_flux, new_force]) + new_iteration = self.anderson(iteration, inc, iter) + new_aux_flux = new_iteration[: self.num_faces] + new_force = new_iteration[self.num_faces :] + + toc = time.time() + time_anderson = toc - tic + # Collect stats stats_i = [time_linearization, time_shrink, time_anderson] From 26f2e4ee463046cc536706caa3aab9de71140f38 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Thu, 14 Sep 2023 11:07:30 +0200 Subject: [PATCH 040/100] MAINT: Add missing import. --- src/darsia/measure/wasserstein.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index 96c4c8d3..c89b6692 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -5,6 +5,7 @@ import time from pathlib import Path +from typing import Union import matplotlib.pyplot as plt import numpy as np From 9e0861d6ec5719b51bd34d7487f1b22fe727fdd0 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Thu, 14 Sep 2023 12:56:42 +0200 Subject: [PATCH 041/100] ENH: Add possibility to use AMG for Bregman as well. --- src/darsia/measure/wasserstein.py | 462 +++++++++++++++++++++++++++++- 1 file changed, 452 insertions(+), 10 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index c89b6692..ec3446b7 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -1448,15 +1448,203 @@ class WassersteinDistanceBregman(VariationalWassersteinDistance): def _problem_specific_setup(self, mass_diff: np.ndarray) -> None: super()._problem_specific_setup(mass_diff) self.L = self.options.get("L", 1.0) - l_scheme_mixed_darcy = sps.bmat( + + # TODO almost as for Newton. Unify! + + def darcy_jacobian(self) -> sps.linalg.LinearOperator: + """Compute the Jacobian of a standard homogeneous Darcy problem. + + The mobility is defined by the used via options["L_init"]. The jacobian is + cached for later use. + + """ + L_init = self.options.get("L", 1.0) # NOTE changed! + jacobian = sps.bmat( [ - [self.L * self.mass_matrix_faces, -self.div.T, None], + [L_init * self.mass_matrix_faces, -self.div.T, None], [self.div, None, -self.potential_constraint.T], [None, self.potential_constraint, None], ], format="csc", ) - self.l_scheme_mixed_darcy_lu = sps.linalg.splu(l_scheme_mixed_darcy) + return jacobian + + # TODO same as for Newton. Unify! + + def setup_infrastructure(self) -> None: + """Setup the infrastructure for reduced systems through Gauss elimination. + + Provide internal data structures for the reduced system. + + """ + # Step 1: Compute the jacobian of the Darcy problem + + # The Darcy problem is sufficient + jacobian = self.darcy_jacobian() + + # Step 2: Remove flux blocks through Schur complement approach + + # Build Schur complement wrt. flux-flux block + J_inv = sps.diags(1.0 / jacobian.diagonal()[self.flux_indices]) + D = jacobian[self.num_faces :, : self.num_faces].copy() + schur_complement = D.dot(J_inv.dot(D.T)) + + # Cache divergence matrix + self.D = D.copy() + self.DT = self.D.T.copy() + + # Cache (constant) jacobian subblock + self.jacobian_subblock = jacobian[self.num_faces :, self.num_faces :].copy() + + # Add Schur complement - use this to identify sparsity structure + # Cache the reduced jacobian + self.reduced_jacobian = self.jacobian_subblock + schur_complement + + # Step 3: Remove potential block through Gauss elimination + + # Find row entries to be removed + rm_row_entries = np.arange( + self.reduced_jacobian.indptr[self.constrained_cell_flat_index], + self.reduced_jacobian.indptr[self.constrained_cell_flat_index + 1], + ) + + # Find column entries to be removed + rm_col_entries = np.where( + self.reduced_jacobian.indices == self.constrained_cell_flat_index + )[0] + + # Collect all entries to be removes + rm_indices = np.unique( + np.concatenate((rm_row_entries, rm_col_entries)).astype(int) + ) + # Cache for later use in remove_lagrange_multiplier + self.rm_indices = rm_indices + + # Identify rows to be reduced + rm_rows = [ + np.max(np.where(self.reduced_jacobian.indptr <= index)[0]) + for index in rm_indices + ] + + # Reduce data - simply remove + fully_reduced_jacobian_data = np.delete(self.reduced_jacobian.data, rm_indices) + + # Reduce indices - remove and shift + fully_reduced_jacobian_indices = np.delete( + self.reduced_jacobian.indices, rm_indices + ) + fully_reduced_jacobian_indices[ + fully_reduced_jacobian_indices > self.constrained_cell_flat_index + ] -= 1 + + # Reduce indptr - shift and remove + # NOTE: As only a few entries should be removed, this is not too expensive + # and a for loop is used + fully_reduced_jacobian_indptr = self.reduced_jacobian.indptr.copy() + for row in rm_rows: + fully_reduced_jacobian_indptr[row + 1 :] -= 1 + fully_reduced_jacobian_indptr = np.unique(fully_reduced_jacobian_indptr) + + # Make sure two rows are removed and deduce shape of reduced jacobian + assert ( + len(fully_reduced_jacobian_indptr) == len(self.reduced_jacobian.indptr) - 2 + ), "Two rows should be removed." + fully_reduced_jacobian_shape = ( + len(fully_reduced_jacobian_indptr) - 1, + len(fully_reduced_jacobian_indptr) - 1, + ) + + # Cache the fully reduced jacobian + self.fully_reduced_jacobian = sps.csc_matrix( + ( + fully_reduced_jacobian_data, + fully_reduced_jacobian_indices, + fully_reduced_jacobian_indptr, + ), + shape=fully_reduced_jacobian_shape, + ) + + # Cache the indices and indptr + self.fully_reduced_jacobian_indices = fully_reduced_jacobian_indices.copy() + self.fully_reduced_jacobian_indptr = fully_reduced_jacobian_indptr.copy() + self.fully_reduced_jacobian_shape = fully_reduced_jacobian_shape + + # Step 4: Identify inclusions (index arrays) + + # Define reduced system indices wrt full system + self.reduced_system_indices = np.concatenate( + [self.potential_indices, self.lagrange_multiplier_indices] + ) + + # Define fully reduced system indices wrt reduced system - need to remove cell + # (and implicitly lagrange multiplier) + self.fully_reduced_system_indices = np.delete( + np.arange(self.num_cells), self.constrained_cell_flat_index + ) + + # Define fully reduced system indices wrt full system + self.fully_reduced_system_indices_full = self.reduced_system_indices[ + self.fully_reduced_system_indices + ] + + def remove_flux(self, jacobian: sps.csc_matrix, residual: np.ndarray) -> tuple: + """Remove the flux block from the jacobian and residual. + + Args: + jacobian (sps.csc_matrix): jacobian + residual (np.ndarray): residual + + Returns: + tuple: reduced jacobian, reduced residual, inverse of flux block + + """ + # Build Schur complement wrt flux-block + J_inv = sps.diags(1.0 / jacobian.diagonal()[self.flux_indices]) + schur_complement = self.D.dot(J_inv.dot(self.DT)) + + # Gauss eliminiation on matrices + reduced_jacobian = self.jacobian_subblock + schur_complement + + # Gauss elimination on vectors + reduced_residual = residual[self.reduced_system_indices].copy() + reduced_residual -= self.D.dot(J_inv.dot(residual[self.flux_indices])) + + return reduced_jacobian, reduced_residual, J_inv + + def remove_lagrange_multiplier(self, jacobian, residual, solution) -> tuple: + """Shortcut for removing the lagrange multiplier from the reduced jacobian. + + Args: + + solution (np.ndarray): solution, TODO make function independent of solution + + Returns: + tuple: fully reduced jacobian, fully reduced residual + + """ + # Make sure the jacobian is a CSC matrix + assert isinstance(jacobian, sps.csc_matrix), "Jacobian should be a CSC matrix." + + # Effective Gauss-elimination for the particular case of the lagrange multiplier + self.fully_reduced_jacobian.data[:] = np.delete( + self.reduced_jacobian.data.copy(), self.rm_indices + ) + # NOTE: The indices have to be restored if the LU factorization is to be used + # FIXME omit if not required + self.fully_reduced_jacobian.indices = self.fully_reduced_jacobian_indices.copy() + + # Rhs is not affected by Gauss elimination as it is assumed that the residual + # is zero in the constrained cell, and the pressure is zero there as well. + # If not, we need to do a proper Gauss elimination on the right hand side! + if abs(residual[-1]) > 1e-6: + raise NotImplementedError("Implementation requires residual to be zero.") + if abs(solution[self.num_faces + self.constrained_cell_flat_index]) > 1e-6: + raise NotImplementedError("Implementation requires solution to be zero.") + fully_reduced_residual = self.reduced_residual[ + self.fully_reduced_system_indices + ].copy() + + return self.fully_reduced_jacobian, fully_reduced_residual def _shrink( self, @@ -1518,6 +1706,44 @@ def _solve(self, flat_mass_diff): tol_increment = self.options.get("tol_increment", 1e-6) # tol_distance = self.options.get("tol_distance", 1e-6) + # Define linear solver to be used to invert the Darcy systems + self.setup_infrastructure() + linear_solver = self.options.get("linear_solver", "lu") + assert linear_solver in [ + "lu", + "lu-flux-reduced", + "amg-flux-reduced", + "lu-potential", + "amg-potential", + ], f"Linear solver {linear_solver} not supported." + + if linear_solver in ["amg-flux-reduced", "amg-potential"]: + # TODO add possibility for user control + ml_options = { + # B=X.reshape( + # n * n, 1 + # ), # the representation of the near null space (this is a poor choice) + # BH=None, # the representation of the left near null space + "symmetry": "hermitian", # indicate that the matrix is Hermitian + # strength="evolution", # change the strength of connection + "aggregate": "standard", # use a standard aggregation method + "smooth": ( + "jacobi", + {"omega": 4.0 / 3.0, "degree": 2}, + ), # prolongation smoothing + "presmoother": ("block_gauss_seidel", {"sweep": "symmetric"}), + "postsmoother": ("block_gauss_seidel", {"sweep": "symmetric"}), + # improve_candidates=[ + # ("block_gauss_seidel", {"sweep": "symmetric", "iterations": 4}), + # None, + # ], + "max_levels": 4, # maximum number of levels + "max_coarse": 1000, # maximum number on a coarse level + # keep=False, # keep extra operators around in the hierarchy (memory) + } + tol_amg = self.options.get("linear_solver_tol", 1e-6) + res_history_amg = [] + # Relaxation parameter self.L = self.options.get("L", 1.0) rhs = np.concatenate( @@ -1560,14 +1786,109 @@ def _solve(self, flat_mass_diff): dissipation_mode = "cell_arithmetic" weight = self.L shrink_factor = 1.0 / self.L - solution_i = self.l_scheme_mixed_darcy_lu.solve(rhs) + solution_i = np.zeros_like(rhs, dtype=float) + + # Solve linear Darcy problem as initial guess + l_scheme_mixed_darcy = sps.bmat( + [ + [self.L * self.mass_matrix_faces, -self.div.T, None], + [self.div, None, -self.potential_constraint.T], + [None, self.potential_constraint, None], + ], + format="csc", + ) + if linear_solver == "lu": + self.l_scheme_mixed_darcy_solver = sps.linalg.splu(l_scheme_mixed_darcy) + solution_i = self.l_scheme_mixed_darcy_solver.solve(rhs) + + elif linear_solver in ["lu-flux-reduced", "amg-flux-reduced"]: + # Solve potential-multiplier problem + + # Reduce flux block + ( + self.reduced_jacobian, + self.reduced_residual, + jacobian_flux_inv, + ) = self.remove_flux(l_scheme_mixed_darcy, rhs) + + if linear_solver == "lu-flux-reduced": + self.l_scheme_mixed_darcy_solver = sps.linalg.splu( + self.reduced_jacobian + ) + + elif linear_solver == "amg-flux-reduced": + self.l_scheme_mixed_darcy_solver = pyamg.smoothed_aggregation_solver( + self.reduced_jacobian, **ml_options + ) + solution_i[ + self.reduced_system_indices + ] = self.l_scheme_mixed_darcy_solver.solve( + self.reduced_residual, + tol=tol_amg, + residuals=res_history_amg, + ) + + # Compute flux update + solution_i[self.flux_indices] = jacobian_flux_inv.dot( + rhs[self.flux_indices] + + self.DT.dot(solution_i[self.reduced_system_indices]) + ) + + elif linear_solver in ["lu-potential", "amg-potential"]: + # Solve pure potential problem + + # Reduce flux block + ( + self.reduced_jacobian, + self.reduced_residual, + jacobian_flux_inv, + ) = self.remove_flux(l_scheme_mixed_darcy, rhs) + + # Reduce to pure pressure system + ( + self.fully_reduced_jacobian, + self.fully_reduced_residual, + ) = self.remove_lagrange_multiplier( + self.reduced_jacobian, self.reduced_residual, solution_i + ) + + if linear_solver == "lu-potential": + self.l_scheme_mixed_darcy_solver = sps.linalg.splu( + self.fully_reduced_jacobian + ) + solution_i[ + self.fully_reduced_system_indices_full + ] = self.l_scheme_mixed_darcy_solver.solve(self.fully_reduced_residual) + + elif linear_solver == "amg-potential": + self.l_scheme_mixed_darcy_solver = pyamg.smoothed_aggregation_solver( + self.fully_reduced_jacobian, **ml_options + ) + solution_i[ + self.fully_reduced_system_indices_full + ] = self.l_scheme_mixed_darcy_solver.solve( + self.fully_reduced_residual, + tol=tol_amg, + residuals=res_history_amg, + ) + + # Compute flux update + solution_i[self.flux_indices] = jacobian_flux_inv.dot( + rhs[self.flux_indices] + + self.DT.dot(solution_i[self.reduced_system_indices]) + ) + + else: + raise NotImplementedError(f"Linear solver {linear_solver} not supported") + + # Extract intial values old_flux, _, _ = self.split_solution(solution_i) old_aux_flux = self._shrink(old_flux, shrink_factor, shrink_mode) old_force = old_flux - old_aux_flux old_distance = self.l1_dissipation(old_flux, dissipation_mode) for iter in range(num_iter): - if True: + if False: # std split Bregman method # 1. Make relaxation step (solve quadratic optimization problem) @@ -1650,7 +1971,10 @@ def _solve(self, flat_mass_diff): elif True: # Bregman split with updated weight update_cond = self.options.get("bregman_update_cond", lambda iter: True) - if update_cond(iter): + update_solver = update_cond(iter) + if update_solver: + # TODO: self._update_weight(old_flux) + # Update weight as the inverse of the norm of the flux old_flux_norm = np.maximum( self.vector_face_flux_norm(old_flux, "face_arithmetic"), @@ -1669,7 +1993,10 @@ def _solve(self, flat_mass_diff): ], format="csc", ) - self.l_scheme_mixed_darcy_lu = sps.linalg.splu(l_scheme_mixed_darcy) + + tic = time.time() + + time_setup = time.time() - tic # 1. Make relaxation step (solve quadratic optimization problem) tic = time.time() @@ -1677,9 +2004,124 @@ def _solve(self, flat_mass_diff): rhs_i[: self.num_faces] = weight * self.mass_matrix_faces.dot( old_aux_flux - old_force ) - new_flux, _, _ = self.split_solution( - self.l_scheme_mixed_darcy_lu.solve(rhs_i) - ) + solution_i = np.zeros_like(rhs_i, dtype=float) + + if linear_solver == "lu": + if update_solver: + self.l_scheme_mixed_darcy_solver = sps.linalg.splu( + l_scheme_mixed_darcy + ) + solution_i = self.l_scheme_mixed_darcy_solver.solve(rhs_i) + + elif linear_solver in ["lu-flux-reduced", "amg-flux-reduced"]: + # Solve potential-multiplier problem + + # Reduce flux block + ( + self.reduced_jacobian, + self.reduced_residual, + jacobian_flux_inv, + ) = self.remove_flux(l_scheme_mixed_darcy, rhs_i) + + if linear_solver == "lu-flux-reduced": + if update_solver: + self.l_scheme_mixed_darcy_solver = sps.linalg.splu( + self.reduced_jacobian + ) + solution_i[ + self.reduced_system_indices + ] = self.l_scheme_mixed_darcy_solver.solve( + self.reduced_residual + ) + + elif linear_solver == "amg-flux-reduced": + if update_solver: + self.l_scheme_mixed_darcy_solver = ( + pyamg.smoothed_aggregation_solver( + self.reduced_jacobian, **ml_options + ) + ) + solution_i[ + self.reduced_system_indices + ] = self.l_scheme_mixed_darcy_solver.solve( + self.reduced_residual, + tol=tol_amg, + residuals=res_history_amg, + ) + + # Compute flux update + solution_i[self.flux_indices] = jacobian_flux_inv.dot( + rhs_i[self.flux_indices] + + self.DT.dot(solution_i[self.reduced_system_indices]) + ) + + elif linear_solver in ["lu-potential", "amg-potential"]: + # Solve pure potential problem + + # Reduce flux block + ( + self.reduced_jacobian, + self.reduced_residual, + jacobian_flux_inv, + ) = self.remove_flux(l_scheme_mixed_darcy, rhs_i) + + # Reduce to pure pressure system + ( + self.fully_reduced_jacobian, + self.fully_reduced_residual, + ) = self.remove_lagrange_multiplier( + self.reduced_jacobian, self.reduced_residual, solution_i + ) + + if linear_solver == "lu-potential": + if update_solver: + self.l_scheme_mixed_darcy_solver = sps.linalg.splu( + self.fully_reduced_jacobian + ) + solution_i[ + self.fully_reduced_system_indices_full + ] = self.l_scheme_mixed_darcy_solver.solve( + self.fully_reduced_residual + ) + + elif linear_solver == "amg-potential": + if update_solver: + self.l_scheme_mixed_darcy_solver = ( + pyamg.smoothed_aggregation_solver( + self.fully_reduced_jacobian, **ml_options + ) + ) + time_setup = time.time() - tic + tic = time.time() + solution_i[ + self.fully_reduced_system_indices_full + ] = self.l_scheme_mixed_darcy_solver.solve( + self.fully_reduced_residual, + tol=tol_amg, + residuals=res_history_amg, + ) + + # Compute flux update + solution_i[self.flux_indices] = jacobian_flux_inv.dot( + rhs_i[self.flux_indices] + + self.DT.dot(solution_i[self.reduced_system_indices]) + ) + else: + raise NotImplementedError( + f"Linear solver {linear_solver} not supported" + ) + + # Diagnostics + if linear_solver in ["amg-flux-reduced", "amg-potential"]: + if self.options.get("linear_solver_verbosity", False): + num_amg_iter = len(res_history_amg) + res_amg = res_history_amg[-1] + print(ml) + print( + f"#AMG iterations: {num_amg_iter}; Residual after AMG step: {res_amg}" + ) + + new_flux, _, _ = self.split_solution(solution_i) time_linearization = time.time() - tic # 2. Shrink step for vectorial fluxes. To comply with the RT0 setting, the From 91139acd6ab27b8a92ba0559f050be2d600721da Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Fri, 15 Sep 2023 08:48:31 +0200 Subject: [PATCH 042/100] MAINT: Remove hardcoded plotting specifications. --- src/darsia/measure/wasserstein.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index ec3446b7..b61078c5 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -790,7 +790,7 @@ def _plot_solution( ) plt.xlabel("x [m]") plt.ylabel("y [m]") - plt.ylim(top=0.08) # TODO rm? + # plt.ylim(top=0.08) # TODO rm? if save_plot: plt.savefig( folder + "/" + name + "_beckman_solution_potential.png", @@ -800,7 +800,7 @@ def _plot_solution( # Plot the fluxes plt.figure("Beckman solution fluxes") - plt.pcolormesh(X, Y, mass_diff, cmap="turbo", vmin=-1, vmax=3.5) + plt.pcolormesh(X, Y, mass_diff, cmap="turbo") # , vmin=-1, vmax=3.5) plt.colorbar(label="mass difference") plt.quiver( X[::resolution, ::resolution], @@ -815,7 +815,7 @@ def _plot_solution( ) plt.xlabel("x [m]") plt.ylabel("y [m]") - plt.ylim(top=0.08) + # plt.ylim(top=0.08) plt.text( 0.0025, 0.075, @@ -838,7 +838,7 @@ def _plot_solution( plt.colorbar(label="flux modulus") plt.xlabel("x [m]") plt.ylabel("y [m]") - plt.ylim(top=0.08) # TODO rm? + # plt.ylim(top=0.08) # TODO rm? if save_plot: plt.savefig( folder + "/" + name + "_beckman_solution_transport_density.png", From 4f338f2be8c37000ff975b6bf8900817d8fb5c55 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Fri, 15 Sep 2023 08:50:16 +0200 Subject: [PATCH 043/100] MAINT: Make Bregman convergence criterion depending on distance --- src/darsia/measure/wasserstein.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index b61078c5..56255768 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -1704,7 +1704,7 @@ def _solve(self, flat_mass_diff): num_iter = self.options.get("num_iter", 100) tol_residual = self.options.get("tol_residual", 1e-6) tol_increment = self.options.get("tol_increment", 1e-6) - # tol_distance = self.options.get("tol_distance", 1e-6) + tol_distance = self.options.get("tol_distance", 1e-6) # Define linear solver to be used to invert the Darcy systems self.setup_infrastructure() @@ -2216,7 +2216,13 @@ def _solve(self, flat_mass_diff): if new_distance > old_distance: num_neg_diff += 1 - # Increase L if stagnating of the distance increases too often. + # TODO include criterion build on staganation of the solution + if iter > 1 and ( + (error[0] < tol_residual and error[4] < tol_increment) + or error[5] < tol_distance + ): + break + update_l = self.options.get("update_l", False) # if update_l: # tol_distance = self.options.get("tol_distance", 1e-12) @@ -2247,10 +2253,6 @@ def _solve(self, flat_mass_diff): # if self.L > L_max: # break - # TODO include criterion build on staganation of the solution - if iter > 1 and ((error[0] < tol_residual and error[4] < tol_increment)): - break - # Update Bregman variables old_flux = new_flux.copy() old_aux_flux = new_aux_flux.copy() From fdca8e2f296a1cfe69c2c1736af262743b24df3d Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Fri, 15 Sep 2023 11:03:31 +0200 Subject: [PATCH 044/100] TST: Add test for wasserstein computations. --- tests/unit/test_wasserstein.py | 98 ++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 tests/unit/test_wasserstein.py diff --git a/tests/unit/test_wasserstein.py b/tests/unit/test_wasserstein.py new file mode 100644 index 00000000..5ea44ac7 --- /dev/null +++ b/tests/unit/test_wasserstein.py @@ -0,0 +1,98 @@ +"""Example for Wasserstein computations moving a square to another location.""" + +import numpy as np +import pytest + +import darsia + +# Coarse src image +rows = 10 +cols = rows +src_square = np.zeros((rows, cols), dtype=float) +src_square[2:5, 2:5] = 1 +meta = {"width": 1, "height": 1, "space_dim": 2, "scalar": True} +src_image = darsia.Image(src_square, **meta) + +# Coarse dst image +dst_squares = np.zeros((rows, cols), dtype=float) +dst_squares[1:3, 1:2] = 1 +dst_squares[4:7, 7:9] = 1 +dst_image = darsia.Image(dst_squares, **meta) + +# Rescale +shape_meta = src_image.shape_metadata() +geometry = darsia.Geometry(**shape_meta) +src_image.img /= geometry.integrate(src_image) +dst_image.img /= geometry.integrate(dst_image) + +# Reference value for comparison +true_distance = 0.379543951823 + +# Linearization +newton_options = { + # Scheme + "L": 1e-9, +} +bregman_options = { + # Scheme + "L": 1, + "bregman_update_cond": lambda iter: iter % 20 == 0, +} +linearizations = { + "newton": newton_options, + "bregman": bregman_options, +} + +# Acceleration +off_aa = { + # Nonlinear solver + "aa_depth": 0, + "aa_restart": None, +} +on_aa = { + # Nonlinear solver + "aa_depth": 5, + "aa_restart": 5, +} +accelerations = [off_aa, on_aa] + +# Linear solver +lu_options = { + # Linear solver + "linear_solver": "lu" +} +amg_options = { + "linear_solver": "amg-potential", + "linear_solver_tol": 1e-6, +} +solvers = [lu_options, amg_options] + +# General options +options = { + # Solver parameters + "regularization": 1e-16, + # Scheme + "lumping": True, + # Performance control + "num_iter": 400, + "tol_residual": 1e-10, + "tol_increment": 1e-6, + "tol_distance": 1e-10, + # Output + "verbose": False, +} + + +@pytest.mark.parametrize("a_key", range(len(accelerations))) +@pytest.mark.parametrize("s_key", range(len(solvers))) +@pytest.mark.parametrize("method", ["newton", "bregman"]) +def test_newton(a_key, s_key, method): + """Test all combinations.""" + options.update(linearizations[method]) + options.update(accelerations[a_key]) + options.update(solvers[s_key]) + distance, _, _, _, status = darsia.wasserstein_distance( + src_image, dst_image, options=options, method=method, return_solution=True + ) + assert np.isclose(distance, true_distance, atol=1e-5) + assert status["converged"] From 52d007321e0320f9be2f09c068572650ff249bdd Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Fri, 15 Sep 2023 11:25:59 +0200 Subject: [PATCH 045/100] TST: Enhance wasserstein test and differ between different BRegman modes --- tests/unit/test_wasserstein.py | 73 +++++++++++++++++++++++++++++----- 1 file changed, 64 insertions(+), 9 deletions(-) diff --git a/tests/unit/test_wasserstein.py b/tests/unit/test_wasserstein.py index 5ea44ac7..4168cc7d 100644 --- a/tests/unit/test_wasserstein.py +++ b/tests/unit/test_wasserstein.py @@ -33,14 +33,28 @@ # Scheme "L": 1e-9, } -bregman_options = { +bregman_std_options = { # Scheme "L": 1, +} +bregman_reordered_options = { + # Scheme + "L": 1, + "bregman_mode": "reordered", +} +bregman_adaptive_options = { + # Scheme + "L": 1, + "bregman_mode": "adaptive", "bregman_update_cond": lambda iter: iter % 20 == 0, } linearizations = { - "newton": newton_options, - "bregman": bregman_options, + "newton": [newton_options], + "bregman": [ + bregman_std_options, + bregman_reordered_options, + bregman_adaptive_options, + ], } # Acceleration @@ -63,7 +77,7 @@ } amg_options = { "linear_solver": "amg-potential", - "linear_solver_tol": 1e-6, + "linear_solver_tol": 1e-8, } solvers = [lu_options, amg_options] @@ -85,14 +99,55 @@ @pytest.mark.parametrize("a_key", range(len(accelerations))) @pytest.mark.parametrize("s_key", range(len(solvers))) -@pytest.mark.parametrize("method", ["newton", "bregman"]) -def test_newton(a_key, s_key, method): - """Test all combinations.""" - options.update(linearizations[method]) +def test_newton(a_key, s_key): + """Test all combinations for Newton.""" + options.update(newton_options) + options.update(accelerations[a_key]) + options.update(solvers[s_key]) + distance, _, _, _, status = darsia.wasserstein_distance( + src_image, dst_image, options=options, method="newton", return_solution=True + ) + assert np.isclose(distance, true_distance, atol=1e-5) + assert status["converged"] + + +@pytest.mark.parametrize("a_key", range(len(accelerations))) +@pytest.mark.parametrize("s_key", [0]) # TODO range(len(solvers))) +def test_std_bregman(a_key, s_key): + """Test all combinations for std Bregman.""" + options.update(bregman_std_options) + options.update(accelerations[a_key]) + options.update(solvers[s_key]) + distance, _, _, _, status = darsia.wasserstein_distance( + src_image, dst_image, options=options, method="bregman", return_solution=True + ) + assert np.isclose(distance, true_distance, atol=1e-2) # TODO + assert status["converged"] + + +@pytest.mark.parametrize("a_key", range(len(accelerations))) +@pytest.mark.parametrize("s_key", [0]) # TODO range(len(solvers))) +def test_reordered_bregman(a_key, s_key): + """Test all combinations for reordered Bregman.""" + options.update(bregman_reordered_options) + options.update(accelerations[a_key]) + options.update(solvers[s_key]) + distance, _, _, _, status = darsia.wasserstein_distance( + src_image, dst_image, options=options, method="bregman", return_solution=True + ) + assert np.isclose(distance, true_distance, atol=1e-2) # TODO + assert status["converged"] + + +@pytest.mark.parametrize("a_key", range(len(accelerations))) +@pytest.mark.parametrize("s_key", range(len(solvers))) +def test_adaptive_bregman(a_key, s_key): + """Test all combinations for adaptive Bregman.""" + options.update(bregman_adaptive_options) options.update(accelerations[a_key]) options.update(solvers[s_key]) distance, _, _, _, status = darsia.wasserstein_distance( - src_image, dst_image, options=options, method=method, return_solution=True + src_image, dst_image, options=options, method="bregman", return_solution=True ) assert np.isclose(distance, true_distance, atol=1e-5) assert status["converged"] From c8cbf53bc2f7feaa110dcefcb5daa93d97f75e3b Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Sat, 16 Sep 2023 07:13:45 +0200 Subject: [PATCH 046/100] STY: black and flake8 --- .../pet_simulations_comparison_block_b.py | 56 ++++++++++--------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/examples/paper/pet_simulations_comparison_block_b.py b/examples/paper/pet_simulations_comparison_block_b.py index 8c4108bc..5456c94c 100644 --- a/examples/paper/pet_simulations_comparison_block_b.py +++ b/examples/paper/pet_simulations_comparison_block_b.py @@ -336,7 +336,7 @@ def read_vtu_images(vtu_ind: int) -> darsia.Image: """Read-in all available VTU data.""" # This corresponds approx. to (only temporary - not used further): - vtu_time = 0 # not relevant here. + vtu_time = 0 # not relevant here. # Find the corresponding files vtu_images_2d = Path(f"data/vtu/block-b/data_2_{str(vtu_ind).zfill(6)}.vtu") @@ -446,9 +446,7 @@ def single_plot( if mode == "sum": image = darsia.reduce_axis(full_image, axis="z", mode="average") else: - image = darsia.reduce_axis( - full_image, axis="z", mode="slice", slice_idx=20 - ) + image = darsia.reduce_axis(full_image, axis="z", mode="slice", slice_idx=20) # Define some plotting options (font, x and ylabels) # matplotlib.rcParams.update({"font.size": 14}) @@ -463,7 +461,6 @@ def single_plot( ) vmax = 1.25 vmin = 0 - contourlevels = [0.045 * vmax, 0.055 * vmax] cmap = "turbo" # Plot the data with contour line. @@ -490,13 +487,14 @@ def single_plot( plt.show() + def qualitative_comparison( mode: str, full_dicom_image: darsia.Image, full_vtu_image: darsia.Image, add_on: str, image_path: Path, - colorbar: bool= False + colorbar: bool = False, ): ############################################################################## # Plot the reconstructed vtu data, vtu plus Gaussian noise, and the dicom data. @@ -604,9 +602,13 @@ def qualitative_comparison( dicom_concentration, vtu_concentration_3d ) aligned_dicom_concentration.save("data/npz/block-b/aligned_dicom_concentration.npz") - aligned_vtu_concentration.save(f"data/npz/block-b/aligned_vtu_{vtu_ind}_concentration.npz") + aligned_vtu_concentration.save( + f"data/npz/block-b/aligned_vtu_{vtu_ind}_concentration.npz" + ) else: - aligned_dicom_concentration = darsia.imread("data/npz/block-b/aligned_dicom_concentration.npz") + aligned_dicom_concentration = darsia.imread( + "data/npz/block-b/aligned_dicom_concentration.npz" + ) aligned_vtu_concentration = darsia.imread( f"data/npz/block-b/aligned_vtu_{vtu_ind}_concentration.npz" ) @@ -630,8 +632,8 @@ def rescale_data(image, ref_integral): # Define dicom concentration with same mass dicom_concentration_3d = aligned_dicom_concentration.copy() dicom_concentration_3d = rescale_data(dicom_concentration_3d, vtu_3d_integral) -#single_plot("slice", dicom_concentration_3d, "noisy", "plots/block-b/lab_data_slice.png") -#single_plot("sum", dicom_concentration_3d, "noisy", "plots/block-b/lab_data_avg.png") +# single_plot("slice", dicom_concentration_3d, "noisy", "plots/block-b/lab_data_slice.png") +# single_plot("sum", dicom_concentration_3d, "noisy", "plots/block-b/lab_data_avg.png") # Define mask (omega) for trust for regularization heterogeneous_omega = True @@ -642,7 +644,7 @@ def rescale_data(image, ref_integral): omega = np.minimum(dicom_rescaled.img, omega_bound) mask_zero = dicom_rescaled.img < 1e-4 omega[mask_zero] = 10 - #plot_slice(omega) + # plot_slice(omega) # Save plot reduced_dicom_concentration_2d = darsia.reduce_axis( @@ -662,7 +664,9 @@ def rescale_data(image, ref_integral): axs_mask.set_aspect("equal") axs_mask.set_xlabel("x [m]") # , fontsize=14) axs_mask.set_ylabel("y [m]") # , fontsize=14) - fig_mask.colorbar(cm.ScalarMappable(cmap="turbo"), ax=axs_mask, label="log($\omega_f$)") + fig_mask.colorbar( + cm.ScalarMappable(cmap="turbo"), ax=axs_mask, label="log($\omega_f$)" + ) fig_mask.savefig("plots/block-b/omega.png", dpi=500, transparent=True) plt.close() else: @@ -680,7 +684,9 @@ def rescale_data(image, ref_integral): dim=3, solver=darsia.CG(maxiter=10000, tol=1e-5), ) - h1_reg_dicom_concentration_3d.save(f"data/npz/block-b/h1_reg_dicom_concentration_{suffix}.npz") + h1_reg_dicom_concentration_3d.save( + f"data/npz/block-b/h1_reg_dicom_concentration_{suffix}.npz" + ) else: h1_reg_dicom_concentration_3d = darsia.imread( f"data/npz/block-b/h1_reg_dicom_concentration_{suffix}.npz" @@ -718,29 +724,29 @@ def rescale_data(image, ref_integral): # Make qualitative comparisons if True: - #plot_sum( + # plot_sum( # [ # vtu_concentration_3d, # dicom_concentration_3d, # h1_reg_dicom_concentration_3d, # tvd_reg_dicom_concentration_3d, # ] - #) - #plot_slice( + # ) + # plot_slice( # [ # vtu_concentration_3d, # dicom_concentration_3d, # h1_reg_dicom_concentration_3d, # tvd_reg_dicom_concentration_3d, # ] - #) - #qualitative_comparison( + # ) + # qualitative_comparison( # "sum", # dicom_concentration_3d, # vtu_concentration_3d, # "noisy", # "plots/block-b/pure_dicom_avg.png", - #) + # ) qualitative_comparison( "slice", dicom_concentration_3d, @@ -748,13 +754,13 @@ def rescale_data(image, ref_integral): "noisy", "plots/block-b/pure_dicom_slice.png", ) - #qualitative_comparison( + # qualitative_comparison( # "sum", # h1_reg_dicom_concentration_3d, # vtu_concentration_3d, # "H1", # f"plots/block-b/h1_reg_dicom_avg_{suffix}.png", - #) + # ) qualitative_comparison( "slice", h1_reg_dicom_concentration_3d, @@ -768,7 +774,7 @@ def rescale_data(image, ref_integral): vtu_concentration_3d, "TVD", f"plots/block-b/tvd_{isotropic_suffix}_reg_dicom_avg_{suffix}.png", - colorbar= True + colorbar=True, ) qualitative_comparison( "slice", @@ -833,13 +839,13 @@ def rescale_slice(image: darsia.Image, ref_integral) -> darsia.Image: "plot_solution": True, } distance_dicom_vtu = darsia.wasserstein_distance( - dicom_slice, vtu_slice, name="plots/block-b/pure dicom vs. vtu", **kwargs + dicom_slice, vtu_slice, name="noisy vs. simulation", **kwargs ) distance_h1_dicom_vtu = darsia.wasserstein_distance( - h1_reg_dicom_slice, vtu_slice, name="plots/block-b/h1 reg dicom vs. vtu", **kwargs + h1_reg_dicom_slice, vtu_slice, name="H1 vs simulation", **kwargs ) distance_tvd_dicom_vtu = darsia.wasserstein_distance( - tvd_reg_dicom_slice, vtu_slice, name="plots/block-b/tvd reg dicom vs. vtu", **kwargs + tvd_reg_dicom_slice, vtu_slice, name="TVD vs simulation", **kwargs ) print("The distances:") From aec4905f61e70ab53be6de09f47b18f18dff4623 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Sat, 16 Sep 2023 07:48:15 +0200 Subject: [PATCH 047/100] MAINT/STY: Add user-access to steer bregman modes. --- src/darsia/measure/wasserstein.py | 93 ++++++++++++++++++------------- 1 file changed, 53 insertions(+), 40 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index 56255768..f3b38a77 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -1888,7 +1888,8 @@ def _solve(self, flat_mass_diff): old_distance = self.l1_dissipation(old_flux, dissipation_mode) for iter in range(num_iter): - if False: + bregman_mode = self.options.get("bregman_mode", "standard") + if bregman_mode == "standard": # std split Bregman method # 1. Make relaxation step (solve quadratic optimization problem) @@ -1898,7 +1899,7 @@ def _solve(self, flat_mass_diff): old_aux_flux - old_force ) new_flux, _, _ = self.split_solution( - self.l_scheme_mixed_darcy_lu.solve(rhs_i) + self.l_scheme_mixed_darcy_solver.solve(rhs_i) ) time_linearization = time.time() - tic @@ -1928,7 +1929,7 @@ def _solve(self, flat_mass_diff): toc = time.time() time_anderson = toc - tic - elif False: + elif bregman_mode == "reordered": # Reordered split Bregman method # 1. Shrink step for vectorial fluxes. To comply with the RT0 setting, the @@ -1950,7 +1951,7 @@ def _solve(self, flat_mass_diff): new_aux_flux - new_force ) new_flux, _, _ = self.split_solution( - self.l_scheme_mixed_darcy_lu.solve(rhs_i) + self.l_scheme_mixed_darcy_solver.solve(rhs_i) ) time_linearization = time.time() - tic @@ -1968,9 +1969,11 @@ def _solve(self, flat_mass_diff): toc = time.time() time_anderson = toc - tic - elif True: + elif bregman_mode == "adaptive": # Bregman split with updated weight - update_cond = self.options.get("bregman_update_cond", lambda iter: True) + update_cond = self.options.get( + "bregman_update_cond", lambda iter: False + ) update_solver = update_cond(iter) if update_solver: # TODO: self._update_weight(old_flux) @@ -1994,10 +1997,6 @@ def _solve(self, flat_mass_diff): format="csc", ) - tic = time.time() - - time_setup = time.time() - tic - # 1. Make relaxation step (solve quadratic optimization problem) tic = time.time() rhs_i = rhs.copy() @@ -2091,7 +2090,7 @@ def _solve(self, flat_mass_diff): self.fully_reduced_jacobian, **ml_options ) ) - time_setup = time.time() - tic + # time_setup = time.time() - tic tic = time.time() solution_i[ self.fully_reduced_system_indices_full @@ -2116,9 +2115,10 @@ def _solve(self, flat_mass_diff): if self.options.get("linear_solver_verbosity", False): num_amg_iter = len(res_history_amg) res_amg = res_history_amg[-1] - print(ml) + print(self.l_scheme_mixed_darcy_solver) print( - f"#AMG iterations: {num_amg_iter}; Residual after AMG step: {res_amg}" + f"""#AMG iterations: {num_amg_iter}; Residual after """ + f"""AMG step: {res_amg}""" ) new_flux, _, _ = self.split_solution(solution_i) @@ -2150,6 +2150,9 @@ def _solve(self, flat_mass_diff): toc = time.time() time_anderson = toc - tic + else: + raise NotImplementedError(f"Bregman mode {bregman_mode} not supported.") + # Collect stats stats_i = [time_linearization, time_shrink, time_anderson] @@ -2170,7 +2173,7 @@ def _solve(self, flat_mass_diff): force = np.linalg.norm(new_force, 2) # Compute the error: - # - residual of mass conservation equation - should be always zero if exact solver used + # - residual of mass conservation equation - zero only if exact solver used # - force # - flux increment # - aux increment @@ -2223,35 +2226,45 @@ def _solve(self, flat_mass_diff): ): break - update_l = self.options.get("update_l", False) - # if update_l: - # tol_distance = self.options.get("tol_distance", 1e-12) - # max_iter_increase_diff = self.options.get("max_iter_increase_diff", 20) - # l_factor = self.options.get("l_factor", 2) - # if ( - # abs(new_distance - old_distance) < tol_distance - # or num_neg_diff > max_iter_increase_diff - # ): - # # Update L - # self.L = self.L * l_factor + # TODO rm? + # update_l = self.options.get("update_l", False) + # if update_l: + # tol_distance = self.options.get("tol_distance", 1e-12) + # max_iter_increase_diff = self.options.get( + # "max_iter_increase_diff", + # 20 + # ) + # l_factor = self.options.get("l_factor", 2) + # if ( + # abs(new_distance - old_distance) < tol_distance + # or num_neg_diff > max_iter_increase_diff + # ): + # # Update L + # self.L = self.L * l_factor # - # # Update linear system - # l_scheme_mixed_darcy = sps.bmat( - # [ - # [self.L * self.mass_matrix_faces, -self.div.T, None], - # [self.div, None, -self.potential_constraint.T], - # [None, self.potential_constraint, None], - # ], - # format="csc", - # ) - # self.l_scheme_mixed_darcy_lu = sps.linalg.splu(l_scheme_mixed_darcy) + # # Update linear system + # l_scheme_mixed_darcy = sps.bmat( + # [ + # [ + # self.L * self.mass_matrix_faces, + # -self.div.T, + # None + # ], + # [self.div, None, -self.potential_constraint.T], + # [None, self.potential_constraint, None], + # ], + # format="csc", + # ) + # self.l_scheme_mixed_darcy_lu = ( + # sps.linalg.splu(l_scheme_mixed_darcy) + # ) # - # # Reset stagnation counter - # num_neg_diff = 0 + # # Reset stagnation counter + # num_neg_diff = 0 # - # L_max = self.options.get("L_max", 1e8) - # if self.L > L_max: - # break + # L_max = self.options.get("L_max", 1e8) + # if self.L > L_max: + # break # Update Bregman variables old_flux = new_flux.copy() From 5086f2855dac51dd4142a96ece7de8210cbe9116 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Sat, 16 Sep 2023 07:50:30 +0200 Subject: [PATCH 048/100] MAINT: Exclude 3d wasserstein file - soon obsolete. --- src/darsia/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/darsia/__init__.py b/src/darsia/__init__.py index 122e1bcb..62471fd9 100644 --- a/src/darsia/__init__.py +++ b/src/darsia/__init__.py @@ -13,6 +13,7 @@ from darsia.measure.integration import * from darsia.measure.emd import * from darsia.measure.wasserstein import * +# from darsia.measure.wasserstein3d import * from darsia.utils.box import * from darsia.utils.interpolation import * from darsia.utils.segmentation import * @@ -67,3 +68,4 @@ from darsia.assistants.base_assistant import * from darsia.assistants.rotation_correction_assistant import * from darsia.assistants.subregion_assistant import * +#from darsia.assistants.curvature_correction_assistant import * From 120ffab5de82fe383061e7624320ed297952b4b2 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Sat, 16 Sep 2023 08:17:16 +0200 Subject: [PATCH 049/100] MAINT: Add pyamg to dev requirements. --- requirements-dev.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements-dev.txt b/requirements-dev.txt index 63eef17d..4824de80 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,3 +5,4 @@ flake8 mypy meshio deepdiff +pyamg From c6b53da73fcf56281554fdae80439eb28dd547c1 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Sat, 16 Sep 2023 08:56:47 +0200 Subject: [PATCH 050/100] STY: flake8 --- src/darsia/measure/wasserstein3d.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/darsia/measure/wasserstein3d.py b/src/darsia/measure/wasserstein3d.py index e2b68778..455c6474 100644 --- a/src/darsia/measure/wasserstein3d.py +++ b/src/darsia/measure/wasserstein3d.py @@ -7,7 +7,6 @@ import time -import matplotlib.pyplot as plt import numpy as np import scipy.sparse as sps @@ -543,9 +542,9 @@ def __call__( status["elapsed_time"] = toc - tic # TODO consider suitable visualization? -# # Plot the solution -# if plot_solution: -# self._plot_solution(mass_diff, flux, pressure, mobility) + # # Plot the solution + # if plot_solution: + # self._plot_solution(mass_diff, flux, pressure, mobility) if return_solution: return distance, flux, pressure, mobility, status From 29c46c4294c0517d5686482a1b1f7493462a39b2 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Sat, 16 Sep 2023 11:19:19 +0200 Subject: [PATCH 051/100] STY: black --- src/darsia/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/darsia/__init__.py b/src/darsia/__init__.py index 62471fd9..18738c85 100644 --- a/src/darsia/__init__.py +++ b/src/darsia/__init__.py @@ -13,6 +13,7 @@ from darsia.measure.integration import * from darsia.measure.emd import * from darsia.measure.wasserstein import * + # from darsia.measure.wasserstein3d import * from darsia.utils.box import * from darsia.utils.interpolation import * @@ -68,4 +69,5 @@ from darsia.assistants.base_assistant import * from darsia.assistants.rotation_correction_assistant import * from darsia.assistants.subregion_assistant import * -#from darsia.assistants.curvature_correction_assistant import * + +# from darsia.assistants.curvature_correction_assistant import * From 7628b09e0b7806d465416de50b7d0fad48f27404 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Sat, 16 Sep 2023 11:39:50 +0200 Subject: [PATCH 052/100] MAINT: Init extraction of a grid object to be used for Wasserstein distances --- src/darsia/__init__.py | 2 ++ src/darsia/measure/wasserstein.py | 26 ++++++++++----------- src/darsia/utils/grid.py | 39 +++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 13 deletions(-) create mode 100644 src/darsia/utils/grid.py diff --git a/src/darsia/__init__.py b/src/darsia/__init__.py index 18738c85..1bb8e4d2 100644 --- a/src/darsia/__init__.py +++ b/src/darsia/__init__.py @@ -27,6 +27,8 @@ from darsia.utils.linear_solvers.mg import * from darsia.utils.andersonacceleration import * from darsia.utils.dtype import * +from darsia.utils.grid import * +# from darsia.utils.fv import * from darsia.corrections.basecorrection import * from darsia.corrections.shape.curvature import * from darsia.corrections.shape.affine import * diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index f3b38a77..0a57480f 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -20,6 +20,7 @@ # - improve assembling of operators through partial assembling # - improve stopping criteria # - use better quadrature for l1_dissipation? +# - allow to reuse setup. class VariationalWassersteinDistance(darsia.EMD): @@ -40,9 +41,7 @@ class VariationalWassersteinDistance(darsia.EMD): def __init__( self, - shape: tuple, - voxel_size: list, - dim: int, + grid: darsia.Grid, options: dict = {}, ) -> None: """ @@ -64,11 +63,11 @@ def __init__( """ # Cache geometrical infos - self.shape = shape - self.voxel_size = voxel_size - self.dim = dim + self.shape = grid.shape + self.voxel_size = grid.voxel_size + self.dim = grid.dim - assert dim == 2, "Currently only 2D images are supported." + assert self.dim == 2, "Currently only 2D images are supported." self.options = options self.regularization = self.options.get("regularization", 0.0) @@ -1407,11 +1406,13 @@ def _solve(self, flat_mass_diff: np.ndarray) -> tuple[float, np.ndarray, dict]: convergence_history["distance increment"].append(error[4]) convergence_history["timing"].append(stats_i) + new_distance_faces = self.l1_dissipation(new_flux, "face_arithmetic") if self.verbose: print( "Newton iteration", iter, new_distance, + new_distance_faces, error[0], # residual error[1], # mass conservation residual error[2], # full increment @@ -1424,7 +1425,7 @@ def _solve(self, flat_mass_diff: np.ndarray) -> tuple[float, np.ndarray, dict]: # TODO include criterion build on staganation of the solution if iter > 1 and ( (error[0] < tol_residual and error[2] < tol_increment) - or error[4] < tol_distance + or error[4] < tol_distance # TODO rm the latter ): break @@ -2311,9 +2312,8 @@ def wasserstein_distance( """ if method.lower() in ["newton", "bregman"]: - shape = mass_1.img.shape - voxel_size = mass_1.voxel_size - dim = mass_1.space_dim + # Extract grid - implicitly assume mass_2 to generate same grid + grid: darsia.Grid = darsia.generate_grid(mass_1) # Fetch options options = kwargs.get("options", {}) @@ -2321,9 +2321,9 @@ def wasserstein_distance( return_solution = kwargs.get("return_solution", False) if method.lower() == "newton": - w1 = WassersteinDistanceNewton(shape, voxel_size, dim, options) + w1 = WassersteinDistanceNewton(grid, options) elif method.lower() == "bregman": - w1 = WassersteinDistanceBregman(shape, voxel_size, dim, options) + w1 = WassersteinDistanceBregman(grid, options) return w1( mass_1, mass_2, plot_solution=plot_solution, return_solution=return_solution ) diff --git a/src/darsia/utils/grid.py b/src/darsia/utils/grid.py new file mode 100644 index 00000000..dc01c310 --- /dev/null +++ b/src/darsia/utils/grid.py @@ -0,0 +1,39 @@ +"""Grid utilities.""" + +from typing import Union + +import numpy as np +import scipy.sparse as sps + +import darsia + + +class Grid: + """Tensor grid. + + Attributes: + shape: Shape of grid. + ndim: Number of dimensions. + size: Number of grid points. + + """ + + def __init__(self, shape: tuple, voxel_size: Union[float, list] = 1.0): + """Initialize grid.""" + + self.shape = shape + self.dim = len(shape) + self.size = np.prod(shape) + self.voxel_size = ( + np.array(voxel_size) + if isinstance(voxel_size, list) + else voxel_size * np.ones(self.dim) + ) + assert len(self.voxel_size) == self.dim + + +def generate_grid(image: darsia.Image) -> Grid: + """Get grid object.""" + shape = image.num_voxels + voxel_size = image.voxel_size + return Grid(shape, voxel_size) From 20161fc9635e6306ce409391c0992476d918c60c Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Sun, 17 Sep 2023 10:12:03 +0200 Subject: [PATCH 053/100] MAINT: Copy tensor grid setup from Wasserstein file. --- src/darsia/utils/grid.py | 104 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 2 deletions(-) diff --git a/src/darsia/utils/grid.py b/src/darsia/utils/grid.py index dc01c310..814c09b2 100644 --- a/src/darsia/utils/grid.py +++ b/src/darsia/utils/grid.py @@ -21,9 +21,9 @@ class Grid: def __init__(self, shape: tuple, voxel_size: Union[float, list] = 1.0): """Initialize grid.""" - self.shape = shape + # Cache grid info self.dim = len(shape) - self.size = np.prod(shape) + self.shape = shape self.voxel_size = ( np.array(voxel_size) if isinstance(voxel_size, list) @@ -31,6 +31,106 @@ def __init__(self, shape: tuple, voxel_size: Union[float, list] = 1.0): ) assert len(self.voxel_size) == self.dim + # Define cell and face numbering + self._setup() + + def _setup(self) -> None: + """Define cell and face numbering.""" + + # ! ---- Grid management ---- + + # Define dimensions of the problem and indexing of cells, from here one start + # counting rows from left to right, from top to bottom. + num_cells = np.prod(self.shape) + flat_numbering_cells = np.arange(num_cells, dtype=int) + numbering_cells = flat_numbering_cells.reshape(self.shape) + + # Consider only inner faces; implicitly define indexing of faces (first + # vertical, then horizontal). The counting of vertical faces starts from top to + # bottom and left to right. The counting of horizontal faces starts from left to + # right and top to bottom. + vertical_faces_shape = (self.shape[0], self.shape[1] - 1) + horizontal_faces_shape = (self.shape[0] - 1, self.shape[1]) + num_vertical_faces = np.prod(vertical_faces_shape) + num_horizontal_faces = np.prod(horizontal_faces_shape) + num_faces_axis = [ + num_vertical_faces, + num_horizontal_faces, + ] + num_faces = np.sum(num_faces_axis) + + # Define flat indexing of faces: vertical faces first, then horizontal faces + flat_vertical_faces = np.arange(num_vertical_faces, dtype=int) + flat_horizontal_faces = num_vertical_faces + np.arange( + num_horizontal_faces, dtype=int + ) + vertical_faces = flat_vertical_faces.reshape(vertical_faces_shape) + horizontal_faces = flat_horizontal_faces.reshape(horizontal_faces_shape) + + # Identify vertical faces on top, inner and bottom + self.top_row_vertical_faces = np.ravel(vertical_faces[0, :]) + self.inner_vertical_faces = np.ravel(vertical_faces[1:-1, :]) + self.bottom_row_vertical_faces = np.ravel(vertical_faces[-1, :]) + # Identify horizontal faces on left, inner and right + self.left_col_horizontal_faces = np.ravel(horizontal_faces[:, 0]) + self.inner_horizontal_faces = np.ravel(horizontal_faces[:, 1:-1]) + self.right_col_horizontal_faces = np.ravel(horizontal_faces[:, -1]) + + # ! ---- Connectivity ---- + + # Define connectivity and direction of the normal on faces + connectivity = np.zeros((num_faces, 2), dtype=int) + # Vertical faces to left cells + connectivity[: num_faces_axis[0], 0] = np.ravel(numbering_cells[:, :-1]) + # Vertical faces to right cells + connectivity[: num_faces_axis[0], 1] = np.ravel(numbering_cells[:, 1:]) + # Horizontal faces to top cells + connectivity[num_faces_axis[0] :, 0] = np.ravel(numbering_cells[:-1, :]) + # Horizontal faces to bottom cells + connectivity[num_faces_axis[0] :, 1] = np.ravel(numbering_cells[1:, :]) + + # Define reverse connectivity. Cell to vertical faces + self.connectivity_cell_to_vertical_face = -np.ones((num_cells, 2), dtype=int) + # Left vertical face of cell + self.connectivity_cell_to_vertical_face[ + np.ravel(numbering_cells[:, 1:]), 0 + ] = flat_vertical_faces + # Right vertical face of cell + self.connectivity_cell_to_vertical_face[ + np.ravel(numbering_cells[:, :-1]), 1 + ] = flat_vertical_faces + # Define reverse connectivity. Cell to horizontal faces + self.connectivity_cell_to_horizontal_face = np.zeros((num_cells, 2), dtype=int) + # Top horizontal face of cell + self.connectivity_cell_to_horizontal_face[ + np.ravel(numbering_cells[1:, :]), 0 + ] = flat_horizontal_faces + # Bottom horizontal face of cell + self.connectivity_cell_to_horizontal_face[ + np.ravel(numbering_cells[:-1, :]), 1 + ] = flat_horizontal_faces + + # Info about inner cells + # TODO rm? + inner_cells_with_vertical_faces = np.ravel(numbering_cells[:, 1:-1]) + inner_cells_with_horizontal_faces = np.ravel(numbering_cells[1:-1, :]) + num_inner_cells_with_vertical_faces = len(inner_cells_with_vertical_faces) + num_inner_cells_with_horizontal_faces = len(inner_cells_with_horizontal_faces) + + # ! ---- Cache ---- + # TODO reduce + self.num_faces = num_faces + self.num_cells = num_cells + self.num_vertical_faces = num_vertical_faces + self.num_horizontal_faces = num_horizontal_faces + self.numbering_cells = numbering_cells + self.num_faces_axis = num_faces_axis + self.vertical_faces_shape = vertical_faces_shape + self.horizontal_faces_shape = horizontal_faces_shape + self.connectivity = connectivity + self.flat_vertical_faces = flat_vertical_faces + self.flat_horizontal_faces = flat_horizontal_faces + def generate_grid(image: darsia.Image) -> Grid: """Get grid object.""" From da6a4044f3308e77245992acfcdad5c2984ae43a Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Sun, 17 Sep 2023 10:12:22 +0200 Subject: [PATCH 054/100] MAINT: Provide basic FV utilities (copied from Wasserstein). --- src/darsia/utils/fv.py | 254 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 src/darsia/utils/fv.py diff --git a/src/darsia/utils/fv.py b/src/darsia/utils/fv.py new file mode 100644 index 00000000..0e9d0f99 --- /dev/null +++ b/src/darsia/utils/fv.py @@ -0,0 +1,254 @@ +"""Finite volume utilities.""" + +import numpy as np +import scipy.sparse as sps + +import darsia + + +class FVDivergence: + """Finite volume divergence operator.""" + + def __init__(self, grid: darsia.Grid) -> None: + # Define sparse divergence operator, integrated over elements. + # Note: The global direction of the degrees of freedom is hereby fixed for all + # faces. Fluxes across vertical faces go from left to right, fluxes across + # horizontal faces go from bottom to top. To oppose the direction of the outer + # normal, the sign of the divergence is flipped for one side of cells for all + # faces. + div_shape = (grid.num_cells, grid.num_faces) + div_data = np.concatenate( + ( + grid.voxel_size[0] * np.ones(grid.num_vertical_faces, dtype=float), + grid.voxel_size[1] * np.ones(grid.num_horizontal_faces, dtype=float), + -grid.voxel_size[0] * np.ones(grid.num_vertical_faces, dtype=float), + -grid.voxel_size[1] * np.ones(grid.num_horizontal_faces, dtype=float), + ) + ) + div_row = np.concatenate( + ( + grid.connectivity[ + grid.flat_vertical_faces, 0 + ], # vertical faces, cells to the left + grid.connectivity[ + grid.flat_horizontal_faces, 0 + ], # horizontal faces, cells to the top + grid.connectivity[ + grid.flat_vertical_faces, 1 + ], # vertical faces, cells to the right (opposite normal) + grid.connectivity[ + grid.flat_horizontal_faces, 1 + ], # horizontal faces, cells to the bottom (opposite normal) + ) + ) + div_col = np.tile(np.arange(grid.num_faces, dtype=int), 2) + div = sps.csc_matrix( + (div_data, (div_row, div_col)), + shape=div_shape, + ) + + # Cache + self.mat = div + + +class FVMass: + def __init__( + self, grid: darsia.Grid, mode: str = "cells", lumping: bool = True + ) -> None: + # Define sparse mass matrix on cells: flat_mass -> flat_mass + if mode == "cells": + mass_matrix = sps.diags( + np.prod(grid.voxel_size) * np.ones(grid.num_cells, dtype=float) + ) + elif mode == "faces": + if lumping: + mass_matrix = 0.5 * sps.diags( + np.prod(grid.voxel_size) * np.ones(grid.num_faces, dtype=float) + ) + else: + # Define true RT0 mass matrix on faces: flat fluxes -> flat fluxes + num_inner_cells_with_vertical_faces = len( + grid.inner_cells_with_vertical_faces + ) + num_inner_cells_with_horizontal_faces = len( + grid.inner_cells_with_horizontal_faces + ) + mass_matrix_shape = (grid.num_faces, grid.num_faces) + mass_matrix_data = np.prod(grid.voxel_size) * np.concatenate( + ( + 2 / 3 * np.ones(grid.num_faces, dtype=float), # all faces + 1 + / 6 + * np.ones( + num_inner_cells_with_vertical_faces, dtype=float + ), # left faces + 1 + / 6 + * np.ones( + num_inner_cells_with_vertical_faces, dtype=float + ), # right faces + 1 + / 6 + * np.ones( + num_inner_cells_with_horizontal_faces, dtype=float + ), # top faces + 1 + / 6 + * np.ones( + num_inner_cells_with_horizontal_faces, dtype=float + ), # bottom faces + ) + ) + mass_matrix_row = np.concatenate( + ( + np.arange(grid.num_faces, dtype=int), + grid.connectivity_cell_to_vertical_face[ + grid.inner_cells_with_vertical_faces, 0 + ], + grid.connectivity_cell_to_vertical_face[ + grid.inner_cells_with_vertical_faces, 1 + ], + grid.connectivity_cell_to_horizontal_face[ + grid.inner_cells_with_horizontal_faces, 0 + ], + grid.connectivity_cell_to_horizontal_face[ + grid.inner_cells_with_horizontal_faces, 1 + ], + ) + ) + mass_matrix_col = np.concatenate( + ( + np.arange(grid.num_faces, dtype=int), + grid.connectivity_cell_to_vertical_face[ + grid.inner_cells_with_vertical_faces, 1 + ], + grid.connectivity_cell_to_vertical_face[ + grid.inner_cells_with_vertical_faces, 0 + ], + grid.connectivity_cell_to_horizontal_face[ + grid.inner_cells_with_horizontal_faces, 1 + ], + grid.connectivity_cell_to_horizontal_face[ + grid.inner_cells_with_horizontal_faces, 0 + ], + ) + ) + + # Define mass matrix in faces + mass_matrix = sps.csc_matrix( + ( + mass_matrix_data, + (mass_matrix_row, mass_matrix_col), + ), + shape=mass_matrix_shape, + ) + + # Cache + self.mat = mass_matrix + + +class FVFaceAverage: + def __init__(self, grid: darsia.Grid) -> None: + # Operator for averaging fluxes on orthogonal, neighboring faces + orthogonal_face_average_shape = (grid.num_faces, grid.num_faces) + orthogonal_face_average_data = 0.25 * np.concatenate( + ( + np.ones( + 2 * len(grid.top_row_vertical_faces) + + 4 * len(grid.inner_vertical_faces) + + 2 * len(grid.bottom_row_vertical_faces) + + 2 * len(grid.left_col_horizontal_faces) + + 4 * len(grid.inner_horizontal_faces) + + 2 * len(grid.right_col_horizontal_faces), + dtype=float, + ), + ) + ) + orthogonal_face_average_rows = np.concatenate( + ( + np.tile(grid.top_row_vertical_faces, 2), + np.tile(grid.inner_vertical_faces, 4), + np.tile(grid.bottom_row_vertical_faces, 2), + np.tile(grid.left_col_horizontal_faces, 2), + np.tile(grid.inner_horizontal_faces, 4), + np.tile(grid.right_col_horizontal_faces, 2), + ) + ) + orthogonal_face_average_cols = np.concatenate( + ( + # top row: left cell -> bottom face + grid.connectivity_cell_to_horizontal_face[ + grid.connectivity[grid.top_row_vertical_faces, 0], 1 + ], + # top row: vertical face -> right cell -> bottom face + grid.connectivity_cell_to_horizontal_face[ + grid.connectivity[grid.top_row_vertical_faces, 1], 1 + ], + # inner rows: vertical face -> left cell -> top face + grid.connectivity_cell_to_horizontal_face[ + grid.connectivity[grid.inner_vertical_faces, 0], 0 + ], + # inner rows: vertical face -> left cell -> bottom face + grid.connectivity_cell_to_horizontal_face[ + grid.connectivity[grid.inner_vertical_faces, 0], 1 + ], + # inner rows: vertical face -> right cell -> top face + grid.connectivity_cell_to_horizontal_face[ + grid.connectivity[grid.inner_vertical_faces, 1], 0 + ], + # inner rows: vertical face -> right cell -> bottom face + grid.connectivity_cell_to_horizontal_face[ + grid.connectivity[grid.inner_vertical_faces, 1], 1 + ], + # bottom row: vertical face -> left cell -> top face + grid.connectivity_cell_to_horizontal_face[ + grid.connectivity[grid.bottom_row_vertical_faces, 0], 0 + ], + # bottom row: vertical face -> right cell -> top face + grid.connectivity_cell_to_horizontal_face[ + grid.connectivity[grid.bottom_row_vertical_faces, 1], 0 + ], + # left column: horizontal face -> top cell -> right face + grid.connectivity_cell_to_vertical_face[ + grid.connectivity[grid.left_col_horizontal_faces, 0], 1 + ], + # left column: horizontal face -> bottom cell -> right face + grid.connectivity_cell_to_vertical_face[ + grid.connectivity[grid.left_col_horizontal_faces, 1], 1 + ], + # inner columns: horizontal face -> top cell -> left face + grid.connectivity_cell_to_vertical_face[ + grid.connectivity[grid.inner_horizontal_faces, 0], 0 + ], + # inner columns: horizontal face -> top cell -> right face + grid.connectivity_cell_to_vertical_face[ + grid.connectivity[grid.inner_horizontal_faces, 0], 1 + ], + # inner columns: horizontal face -> bottom cell -> left face + grid.connectivity_cell_to_vertical_face[ + grid.connectivity[grid.inner_horizontal_faces, 1], 0 + ], + # inner columns: horizontal face -> bottom cell -> right face + grid.connectivity_cell_to_vertical_face[ + grid.connectivity[grid.inner_horizontal_faces, 1], 1 + ], + # right column: horizontal face -> top cell -> left face + grid.connectivity_cell_to_vertical_face[ + grid.connectivity[grid.right_col_horizontal_faces, 0], 0 + ], + # right column: horizontal face -> bottom cell -> left face + grid.connectivity_cell_to_vertical_face[ + grid.connectivity[grid.right_col_horizontal_faces, 1], 0 + ], + ) + ) + orthogonal_face_average = sps.csc_matrix( + ( + orthogonal_face_average_data, + (orthogonal_face_average_rows, orthogonal_face_average_cols), + ), + shape=orthogonal_face_average_shape, + ) + + # Cache + self.mat = orthogonal_face_average From b6a9b56d3e309680da84a212172cebddd9c83ba5 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Sun, 17 Sep 2023 10:13:04 +0200 Subject: [PATCH 055/100] MAINT: Adapt Wasserstein class to extracted grid and fv management. --- src/darsia/measure/wasserstein.py | 424 +++++------------------------- 1 file changed, 70 insertions(+), 354 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index 0a57480f..472cb21a 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -63,7 +63,7 @@ def __init__( """ # Cache geometrical infos - self.shape = grid.shape + self.grid = grid self.voxel_size = grid.voxel_size self.dim = grid.dim @@ -79,311 +79,20 @@ def __init__( def _setup(self) -> None: """Setup of fixed discretization""" - # ! ---- Grid management ---- - - # Define dimensions of the problem and indexing of cells, from here one start - # counting rows from left to right, from top to bottom. - dim_cells = self.shape - num_cells = np.prod(dim_cells) - flat_numbering_cells = np.arange(num_cells, dtype=int) - numbering_cells = flat_numbering_cells.reshape(dim_cells) - - # Define center cell - center_cell = np.array([self.shape[0] // 2, self.shape[1] // 2]).astype(int) - self.flat_center_cell = np.ravel_multi_index(center_cell, dim_cells) - - # Consider only inner faces; implicitly define indexing of faces (first - # vertical, then horizontal). The counting of vertical faces starts from top to - # bottom and left to right. The counting of horizontal faces starts from left to - # right and top to bottom. - vertical_faces_shape = (self.shape[0], self.shape[1] - 1) - horizontal_faces_shape = (self.shape[0] - 1, self.shape[1]) - num_vertical_faces = np.prod(vertical_faces_shape) - num_horizontal_faces = np.prod(horizontal_faces_shape) - num_faces_axis = [ - num_vertical_faces, - num_horizontal_faces, - ] - num_faces = np.sum(num_faces_axis) - - # Define flat indexing of faces: vertical faces first, then horizontal faces - flat_vertical_faces = np.arange(num_vertical_faces, dtype=int) - flat_horizontal_faces = num_vertical_faces + np.arange( - num_horizontal_faces, dtype=int - ) - vertical_faces = flat_vertical_faces.reshape(vertical_faces_shape) - horizontal_faces = flat_horizontal_faces.reshape(horizontal_faces_shape) - - # Identify vertical faces on top, inner and bottom - top_row_vertical_faces = np.ravel(vertical_faces[0, :]) - inner_vertical_faces = np.ravel(vertical_faces[1:-1, :]) - bottom_row_vertical_faces = np.ravel(vertical_faces[-1, :]) - # Identify horizontal faces on left, inner and right - left_col_horizontal_faces = np.ravel(horizontal_faces[:, 0]) - inner_horizontal_faces = np.ravel(horizontal_faces[:, 1:-1]) - right_col_horizontal_faces = np.ravel(horizontal_faces[:, -1]) - - # ! ---- Connectivity ---- - - # Define connectivity and direction of the normal on faces - connectivity = np.zeros((num_faces, 2), dtype=int) - # Vertical faces to left cells - connectivity[: num_faces_axis[0], 0] = np.ravel(numbering_cells[:, :-1]) - # Vertical faces to right cells - connectivity[: num_faces_axis[0], 1] = np.ravel(numbering_cells[:, 1:]) - # Horizontal faces to top cells - connectivity[num_faces_axis[0] :, 0] = np.ravel(numbering_cells[:-1, :]) - # Horizontal faces to bottom cells - connectivity[num_faces_axis[0] :, 1] = np.ravel(numbering_cells[1:, :]) - - # Define reverse connectivity. Cell to vertical faces - connectivity_cell_to_vertical_face = -np.ones((num_cells, 2), dtype=int) - # Left vertical face of cell - connectivity_cell_to_vertical_face[ - np.ravel(numbering_cells[:, 1:]), 0 - ] = flat_vertical_faces - # Right vertical face of cell - connectivity_cell_to_vertical_face[ - np.ravel(numbering_cells[:, :-1]), 1 - ] = flat_vertical_faces - # Define reverse connectivity. Cell to horizontal faces - connectivity_cell_to_horizontal_face = np.zeros((num_cells, 2), dtype=int) - # Top horizontal face of cell - connectivity_cell_to_horizontal_face[ - np.ravel(numbering_cells[1:, :]), 0 - ] = flat_horizontal_faces - # Bottom horizontal face of cell - connectivity_cell_to_horizontal_face[ - np.ravel(numbering_cells[:-1, :]), 1 - ] = flat_horizontal_faces - - # Info about inner cells - # TODO rm? - inner_cells_with_vertical_faces = np.ravel(numbering_cells[:, 1:-1]) - inner_cells_with_horizontal_faces = np.ravel(numbering_cells[1:-1, :]) - num_inner_cells_with_vertical_faces = len(inner_cells_with_vertical_faces) - num_inner_cells_with_horizontal_faces = len(inner_cells_with_horizontal_faces) - # ! ---- Operators ---- - # Define sparse divergence operator, integrated over elements. - # Note: The global direction of the degrees of freedom is hereby fixed for all - # faces. Fluxes across vertical faces go from left to right, fluxes across - # horizontal faces go from bottom to top. To oppose the direction of the outer - # normal, the sign of the divergence is flipped for one side of cells for all - # faces. - div_shape = (num_cells, num_faces) - div_data = np.concatenate( - ( - self.voxel_size[0] * np.ones(num_vertical_faces, dtype=float), - self.voxel_size[1] * np.ones(num_horizontal_faces, dtype=float), - -self.voxel_size[0] * np.ones(num_vertical_faces, dtype=float), - -self.voxel_size[1] * np.ones(num_horizontal_faces, dtype=float), - ) - ) - div_row = np.concatenate( - ( - connectivity[ - flat_vertical_faces, 0 - ], # vertical faces, cells to the left - connectivity[ - flat_horizontal_faces, 0 - ], # horizontal faces, cells to the top - connectivity[ - flat_vertical_faces, 1 - ], # vertical faces, cells to the right (opposite normal) - connectivity[ - flat_horizontal_faces, 1 - ], # horizontal faces, cells to the bottom (opposite normal) - ) - ) - div_col = np.tile(np.arange(num_faces, dtype=int), 2) - self.div = sps.csc_matrix( - (div_data, (div_row, div_col)), - shape=div_shape, - ) + self.div = darsia.FVDivergence(self.grid).mat + """sps.csc_matrix: divergence operator: flat fluxes -> flat potentials""" - # Define sparse mass matrix on cells: flat_mass -> flat_mass - self.mass_matrix_cells = sps.diags( - np.prod(self.voxel_size) * np.ones(num_cells, dtype=float) - ) + self.mass_matrix_cells = darsia.FVMass(self.grid).mat + """sps.csc_matrix: mass matrix on cells: flat potentials -> flat potentials""" - # Define sparse mass matrix on faces: flat fluxes -> flat fluxes lumping = self.options.get("lumping", True) - if lumping: - self.mass_matrix_faces = 0.5 * sps.diags( - np.prod(self.voxel_size) * np.ones(num_faces, dtype=float) - ) - else: - # Define true RT0 mass matrix on faces: flat fluxes -> flat fluxes - mass_matrix_faces_shape = (num_faces, num_faces) - mass_matrix_faces_data = np.prod(self.voxel_size) * np.concatenate( - ( - 2 / 3 * np.ones(num_faces, dtype=float), # all faces - 1 - / 6 - * np.ones( - num_inner_cells_with_vertical_faces, dtype=float - ), # left faces - 1 - / 6 - * np.ones( - num_inner_cells_with_vertical_faces, dtype=float - ), # right faces - 1 - / 6 - * np.ones( - num_inner_cells_with_horizontal_faces, dtype=float - ), # top faces - 1 - / 6 - * np.ones( - num_inner_cells_with_horizontal_faces, dtype=float - ), # bottom faces - ) - ) - mass_matrix_faces_row = np.concatenate( - ( - np.arange(num_faces, dtype=int), - connectivity_cell_to_vertical_face[ - inner_cells_with_vertical_faces, 0 - ], - connectivity_cell_to_vertical_face[ - inner_cells_with_vertical_faces, 1 - ], - connectivity_cell_to_horizontal_face[ - inner_cells_with_horizontal_faces, 0 - ], - connectivity_cell_to_horizontal_face[ - inner_cells_with_horizontal_faces, 1 - ], - ) - ) - mass_matrix_faces_col = np.concatenate( - ( - np.arange(num_faces, dtype=int), - connectivity_cell_to_vertical_face[ - inner_cells_with_vertical_faces, 1 - ], - connectivity_cell_to_vertical_face[ - inner_cells_with_vertical_faces, 0 - ], - connectivity_cell_to_horizontal_face[ - inner_cells_with_horizontal_faces, 1 - ], - connectivity_cell_to_horizontal_face[ - inner_cells_with_horizontal_faces, 0 - ], - ) - ) - self.mass_matrix_faces = sps.csc_matrix( - ( - mass_matrix_faces_data, - (mass_matrix_faces_row, mass_matrix_faces_col), - ), - shape=mass_matrix_faces_shape, - ) + self.mass_matrix_faces = darsia.FVMass(self.grid, "faces", lumping).mat + """sps.csc_matrix: mass matrix on faces: flat fluxes -> flat fluxes""" - # Operator for averaging fluxes on orthogonal, neighboring faces - orthogonal_face_average_shape = (num_faces, num_faces) - orthogonal_face_average_data = 0.25 * np.concatenate( - ( - np.ones( - 2 * len(top_row_vertical_faces) - + 4 * len(inner_vertical_faces) - + 2 * len(bottom_row_vertical_faces) - + 2 * len(left_col_horizontal_faces) - + 4 * len(inner_horizontal_faces) - + 2 * len(right_col_horizontal_faces), - dtype=float, - ), - ) - ) - orthogonal_face_average_rows = np.concatenate( - ( - np.tile(top_row_vertical_faces, 2), - np.tile(inner_vertical_faces, 4), - np.tile(bottom_row_vertical_faces, 2), - np.tile(left_col_horizontal_faces, 2), - np.tile(inner_horizontal_faces, 4), - np.tile(right_col_horizontal_faces, 2), - ) - ) - orthogonal_face_average_cols = np.concatenate( - ( - # top row: left cell -> bottom face - connectivity_cell_to_horizontal_face[ - connectivity[top_row_vertical_faces, 0], 1 - ], - # top row: vertical face -> right cell -> bottom face - connectivity_cell_to_horizontal_face[ - connectivity[top_row_vertical_faces, 1], 1 - ], - # inner rows: vertical face -> left cell -> top face - connectivity_cell_to_horizontal_face[ - connectivity[inner_vertical_faces, 0], 0 - ], - # inner rows: vertical face -> left cell -> bottom face - connectivity_cell_to_horizontal_face[ - connectivity[inner_vertical_faces, 0], 1 - ], - # inner rows: vertical face -> right cell -> top face - connectivity_cell_to_horizontal_face[ - connectivity[inner_vertical_faces, 1], 0 - ], - # inner rows: vertical face -> right cell -> bottom face - connectivity_cell_to_horizontal_face[ - connectivity[inner_vertical_faces, 1], 1 - ], - # bottom row: vertical face -> left cell -> top face - connectivity_cell_to_horizontal_face[ - connectivity[bottom_row_vertical_faces, 0], 0 - ], - # bottom row: vertical face -> right cell -> top face - connectivity_cell_to_horizontal_face[ - connectivity[bottom_row_vertical_faces, 1], 0 - ], - # left column: horizontal face -> top cell -> right face - connectivity_cell_to_vertical_face[ - connectivity[left_col_horizontal_faces, 0], 1 - ], - # left column: horizontal face -> bottom cell -> right face - connectivity_cell_to_vertical_face[ - connectivity[left_col_horizontal_faces, 1], 1 - ], - # inner columns: horizontal face -> top cell -> left face - connectivity_cell_to_vertical_face[ - connectivity[inner_horizontal_faces, 0], 0 - ], - # inner columns: horizontal face -> top cell -> right face - connectivity_cell_to_vertical_face[ - connectivity[inner_horizontal_faces, 0], 1 - ], - # inner columns: horizontal face -> bottom cell -> left face - connectivity_cell_to_vertical_face[ - connectivity[inner_horizontal_faces, 1], 0 - ], - # inner columns: horizontal face -> bottom cell -> right face - connectivity_cell_to_vertical_face[ - connectivity[inner_horizontal_faces, 1], 1 - ], - # right column: horizontal face -> top cell -> left face - connectivity_cell_to_vertical_face[ - connectivity[right_col_horizontal_faces, 0], 0 - ], - # right column: horizontal face -> bottom cell -> left face - connectivity_cell_to_vertical_face[ - connectivity[right_col_horizontal_faces, 1], 0 - ], - ) - ) - self.orthogonal_face_average = sps.csc_matrix( - ( - orthogonal_face_average_data, - (orthogonal_face_average_rows, orthogonal_face_average_cols), - ), - shape=orthogonal_face_average_shape, - ) + self.orthogonal_face_average = darsia.FVFaceAverage(self.grid).mat + """sps.csc_matrix: averaging operator for fluxes on orthogonal faces""" # Define sparse embedding operators, and quick access through indices. # Assume the ordering of the faces is vertical faces first, then horizontal @@ -391,15 +100,19 @@ def _setup(self) -> None: # scalar variable for the lagrange multiplier. self.flux_embedding = sps.csc_matrix( ( - np.ones(num_faces, dtype=float), - (np.arange(num_faces), np.arange(num_faces)), + np.ones(self.grid.num_faces, dtype=float), + (np.arange(self.grid.num_faces), np.arange(self.grid.num_faces)), ), - shape=(num_faces + num_cells + 1, num_faces), + shape=(self.grid.num_faces + self.grid.num_cells + 1, self.grid.num_faces), ) - self.flux_indices = np.arange(num_faces) - self.potential_indices = np.arange(num_faces, num_faces + num_cells) - self.lagrange_multiplier_indices = np.array([num_faces + num_cells], dtype=int) + self.flux_indices = np.arange(self.grid.num_faces) + self.potential_indices = np.arange( + self.grid.num_faces, self.grid.num_faces + self.grid.num_cells + ) + self.lagrange_multiplier_indices = np.array( + [self.grid.num_faces + self.grid.num_cells], dtype=int + ) # ! ---- Utilities ---- aa_depth = self.options.get("aa_depth", 0) @@ -412,26 +125,21 @@ def _setup(self) -> None: else None ) - # ! ---- Cache ---- - self.num_faces = num_faces - self.num_cells = num_cells - self.dim_cells = dim_cells - self.numbering_cells = numbering_cells - self.num_faces_axis = num_faces_axis - self.vertical_faces_shape = vertical_faces_shape - self.horizontal_faces_shape = horizontal_faces_shape - def _problem_specific_setup(self, mass_diff: np.ndarray) -> None: """Resetup of fixed discretization""" # Fix index of center cell - self.constrained_cell_flat_index = self.flat_center_cell + center = 0.5 * np.array(self.grid.shape) + center_cell = center.astype(int) + self.constrained_cell_flat_index = np.ravel_multi_index( + center_cell, self.grid.shape + ) self.potential_constraint = sps.csc_matrix( ( np.ones(1, dtype=float), (np.zeros(1, dtype=int), np.array([self.constrained_cell_flat_index])), ), - shape=(1, self.num_cells), + shape=(1, self.grid.num_cells), dtype=float, ) @@ -458,8 +166,10 @@ def split_solution( """ # Split the solution - flat_flux = solution[: self.num_faces] - flat_potential = solution[self.num_faces : self.num_faces + self.num_cells] + flat_flux = solution[: self.grid.num_faces] + flat_potential = solution[ + self.grid.num_faces : self.grid.num_faces + self.grid.num_cells + ] flat_lagrange_multiplier = solution[-1] return flat_flux, flat_potential, flat_lagrange_multiplier @@ -484,16 +194,16 @@ def face_to_cell(self, flat_flux: np.ndarray) -> np.ndarray: """ # Reshape fluxes - use duality of faces and normals - horizontal_fluxes = flat_flux[: self.num_faces_axis[0]].reshape( - self.vertical_faces_shape + horizontal_fluxes = flat_flux[: self.grid.num_faces_axis[0]].reshape( + self.grid.vertical_faces_shape ) - vertical_fluxes = flat_flux[self.num_faces_axis[0] :].reshape( - self.horizontal_faces_shape + vertical_fluxes = flat_flux[self.grid.num_faces_axis[0] :].reshape( + self.grid.horizontal_faces_shape ) # Determine a cell-based Raviart-Thomas reconstruction of the fluxes, projected # onto piecewise constant functions. - cell_flux = np.zeros((*self.dim_cells, self.dim), dtype=float) + cell_flux = np.zeros((*self.grid.shape, self.dim), dtype=float) # Horizontal fluxes cell_flux[:, :-1, 0] += 0.5 * horizontal_fluxes cell_flux[:, 1:, 0] += 0.5 * horizontal_fluxes @@ -714,7 +424,7 @@ def __call__( # Reshape the fluxes and potential to grid format flux = self.face_to_cell(flat_flux) - potential = flat_potential.reshape(self.dim_cells) + potential = flat_potential.reshape(self.grid.shape) # Determine transport density transport_density = self.transport_density(flux) @@ -767,8 +477,8 @@ def _plot_solution( # Meshgrid Y, X = np.meshgrid( - self.voxel_size[0] * (0.5 + np.arange(self.shape[0] - 1, -1, -1)), - self.voxel_size[1] * (0.5 + np.arange(self.shape[1])), + self.voxel_size[0] * (0.5 + np.arange(self.grid.shape[0] - 1, -1, -1)), + self.voxel_size[1] * (0.5 + np.arange(self.grid.shape[1])), indexing="ij", ) @@ -941,7 +651,7 @@ def setup_infrastructure(self) -> None: # Build Schur complement wrt. flux-flux block J_inv = sps.diags(1.0 / jacobian.diagonal()[self.flux_indices]) - D = jacobian[self.num_faces :, : self.num_faces].copy() + D = jacobian[self.grid.num_faces :, : self.grid.num_faces].copy() schur_complement = D.dot(J_inv.dot(D.T)) # Cache divergence matrix @@ -949,7 +659,9 @@ def setup_infrastructure(self) -> None: self.DT = self.D.T.copy() # Cache (constant) jacobian subblock - self.jacobian_subblock = jacobian[self.num_faces :, self.num_faces :].copy() + self.jacobian_subblock = jacobian[ + self.grid.num_faces :, self.grid.num_faces : + ].copy() # Add Schur complement - use this to identify sparsity structure # Cache the reduced jacobian @@ -1034,7 +746,7 @@ def setup_infrastructure(self) -> None: # Define fully reduced system indices wrt reduced system - need to remove cell # (and implicitly lagrange multiplier) self.fully_reduced_system_indices = np.delete( - np.arange(self.num_cells), self.constrained_cell_flat_index + np.arange(self.grid.num_cells), self.constrained_cell_flat_index ) # Define fully reduced system indices wrt full system @@ -1093,7 +805,7 @@ def remove_lagrange_multiplier(self, jacobian, residual, solution) -> tuple: # If not, we need to do a proper Gauss elimination on the right hand side! if abs(residual[-1]) > 1e-6: raise NotImplementedError("Implementation requires residual to be zero.") - if abs(solution[self.num_faces + self.constrained_cell_flat_index]) > 1e-6: + if abs(solution[self.grid.num_faces + self.constrained_cell_flat_index]) > 1e-6: raise NotImplementedError("Implementation requires solution to be zero.") fully_reduced_residual = self.reduced_residual[ self.fully_reduced_system_indices @@ -1308,7 +1020,7 @@ def _solve(self, flat_mass_diff: np.ndarray) -> tuple[float, np.ndarray, dict]: # Define right hand side rhs = np.concatenate( [ - np.zeros(self.num_faces, dtype=float), + np.zeros(self.grid.num_faces, dtype=float), self.mass_matrix_cells.dot(flat_mass_diff), np.zeros(1, dtype=float), ] @@ -1359,8 +1071,10 @@ def _solve(self, flat_mass_diff: np.ndarray) -> tuple[float, np.ndarray, dict]: # while application to the flux, results in improved performance. tic = time.time() if self.anderson is not None: - solution_i[: self.num_faces] = self.anderson( - solution_i[: self.num_faces], update_i[: self.num_faces], iter + solution_i[: self.grid.num_faces] = self.anderson( + solution_i[: self.grid.num_faces], + update_i[: self.grid.num_faces], + iter, ) toc = time.time() time_anderson = toc - tic @@ -1384,14 +1098,14 @@ def _solve(self, flat_mass_diff: np.ndarray) -> tuple[float, np.ndarray, dict]: error = [ np.linalg.norm(residual_i, 2), [ - np.linalg.norm(residual_i[: self.num_faces], 2), - np.linalg.norm(residual_i[self.num_faces : -1], 2), + np.linalg.norm(residual_i[: self.grid.num_faces], 2), + np.linalg.norm(residual_i[self.grid.num_faces : -1], 2), np.linalg.norm(residual_i[-1:], 2), ], np.linalg.norm(increment, 2), [ - np.linalg.norm(increment[: self.num_faces], 2), - np.linalg.norm(increment[self.num_faces : -1], 2), + np.linalg.norm(increment[: self.grid.num_faces], 2), + np.linalg.norm(increment[self.grid.num_faces : -1], 2), np.linalg.norm(increment[-1:], 2), ], abs(new_distance - old_distance), @@ -1487,7 +1201,7 @@ def setup_infrastructure(self) -> None: # Build Schur complement wrt. flux-flux block J_inv = sps.diags(1.0 / jacobian.diagonal()[self.flux_indices]) - D = jacobian[self.num_faces :, : self.num_faces].copy() + D = jacobian[self.grid.num_faces :, : self.grid.num_faces].copy() schur_complement = D.dot(J_inv.dot(D.T)) # Cache divergence matrix @@ -1495,7 +1209,9 @@ def setup_infrastructure(self) -> None: self.DT = self.D.T.copy() # Cache (constant) jacobian subblock - self.jacobian_subblock = jacobian[self.num_faces :, self.num_faces :].copy() + self.jacobian_subblock = jacobian[ + self.grid.num_faces :, self.grid.num_faces : + ].copy() # Add Schur complement - use this to identify sparsity structure # Cache the reduced jacobian @@ -1580,7 +1296,7 @@ def setup_infrastructure(self) -> None: # Define fully reduced system indices wrt reduced system - need to remove cell # (and implicitly lagrange multiplier) self.fully_reduced_system_indices = np.delete( - np.arange(self.num_cells), self.constrained_cell_flat_index + np.arange(self.grid.num_cells), self.constrained_cell_flat_index ) # Define fully reduced system indices wrt full system @@ -1639,7 +1355,7 @@ def remove_lagrange_multiplier(self, jacobian, residual, solution) -> tuple: # If not, we need to do a proper Gauss elimination on the right hand side! if abs(residual[-1]) > 1e-6: raise NotImplementedError("Implementation requires residual to be zero.") - if abs(solution[self.num_faces + self.constrained_cell_flat_index]) > 1e-6: + if abs(solution[self.grid.num_faces + self.constrained_cell_flat_index]) > 1e-6: raise NotImplementedError("Implementation requires solution to be zero.") fully_reduced_residual = self.reduced_residual[ self.fully_reduced_system_indices @@ -1749,7 +1465,7 @@ def _solve(self, flat_mass_diff): self.L = self.options.get("L", 1.0) rhs = np.concatenate( [ - np.zeros(self.num_faces, dtype=float), + np.zeros(self.grid.num_faces, dtype=float), self.mass_matrix_cells.dot(flat_mass_diff), np.zeros(1, dtype=float), ] @@ -1896,7 +1612,7 @@ def _solve(self, flat_mass_diff): # 1. Make relaxation step (solve quadratic optimization problem) tic = time.time() rhs_i = rhs.copy() - rhs_i[: self.num_faces] = self.L * self.mass_matrix_faces.dot( + rhs_i[: self.grid.num_faces] = self.L * self.mass_matrix_faces.dot( old_aux_flux - old_force ) new_flux, _, _ = self.split_solution( @@ -1924,8 +1640,8 @@ def _solve(self, flat_mass_diff): inc = np.concatenate([aux_inc, force_inc]) iteration = np.concatenate([new_aux_flux, new_force]) new_iteration = self.anderson(iteration, inc, iter) - new_aux_flux = new_iteration[: self.num_faces] - new_force = new_iteration[self.num_faces :] + new_aux_flux = new_iteration[: self.grid.num_faces] + new_force = new_iteration[self.grid.num_faces :] toc = time.time() time_anderson = toc - tic @@ -1948,7 +1664,7 @@ def _solve(self, flat_mass_diff): # 3. Solve linear system with trust in current flux. tic = time.time() rhs_i = rhs.copy() - rhs_i[: self.num_faces] = self.L * self.mass_matrix_faces.dot( + rhs_i[: self.grid.num_faces] = self.L * self.mass_matrix_faces.dot( new_aux_flux - new_force ) new_flux, _, _ = self.split_solution( @@ -1965,8 +1681,8 @@ def _solve(self, flat_mass_diff): iteration = np.concatenate([new_flux, new_force]) # new_flux = self.anderson(new_flux, flux_inc, iter) new_iteration = self.anderson(iteration, inc, iter) - new_flux = new_iteration[: self.num_faces] - new_force = new_iteration[self.num_faces :] + new_flux = new_iteration[: self.grid.num_faces] + new_force = new_iteration[self.grid.num_faces :] toc = time.time() time_anderson = toc - tic @@ -2001,7 +1717,7 @@ def _solve(self, flat_mass_diff): # 1. Make relaxation step (solve quadratic optimization problem) tic = time.time() rhs_i = rhs.copy() - rhs_i[: self.num_faces] = weight * self.mass_matrix_faces.dot( + rhs_i[: self.grid.num_faces] = weight * self.mass_matrix_faces.dot( old_aux_flux - old_force ) solution_i = np.zeros_like(rhs_i, dtype=float) @@ -2145,8 +1861,8 @@ def _solve(self, flat_mass_diff): inc = np.concatenate([aux_inc, force_inc]) iteration = np.concatenate([new_aux_flux, new_force]) new_iteration = self.anderson(iteration, inc, iter) - new_aux_flux = new_iteration[: self.num_faces] - new_force = new_iteration[self.num_faces :] + new_aux_flux = new_iteration[: self.grid.num_faces] + new_force = new_iteration[self.grid.num_faces :] toc = time.time() time_anderson = toc - tic @@ -2162,7 +1878,7 @@ def _solve(self, flat_mass_diff): # Determine the error in the mass conservation equation mass_conservation_residual = np.linalg.norm( - self.div.dot(new_flux) - rhs[self.num_faces : -1], 2 + self.div.dot(new_flux) - rhs[self.grid.num_faces : -1], 2 ) # Determine increments @@ -2275,7 +1991,7 @@ def _solve(self, flat_mass_diff): # TODO solve for potential and multiplier solution_i = np.zeros_like(rhs) - solution_i[: self.num_faces] = new_flux.copy() + solution_i[: self.grid.num_faces] = new_flux.copy() # TODO continue # Define performance metric From 4ae22e3a846ab0921b412c6b15c87a681f0683a8 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Sun, 17 Sep 2023 10:40:08 +0200 Subject: [PATCH 056/100] MAINT: Reorganize setup of Wasserstein distances --- src/darsia/measure/wasserstein.py | 109 +++++++++++++++++------------- src/darsia/utils/grid.py | 7 +- 2 files changed, 64 insertions(+), 52 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index 472cb21a..4c61bd67 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -47,9 +47,7 @@ def __init__( """ Args: - shape (tuple): shape of the image - voxel_size (list): voxel size of the image - dim (int): dimension of the problem + grid (darsia.Grid): tensor grid associated with the images options (dict): options for the solver - num_iter (int): maximum number of iterations. Defaults to 100. - tol (float): tolerance for the stopping criterion. Defaults to 1e-6. @@ -79,56 +77,43 @@ def __init__( def _setup(self) -> None: """Setup of fixed discretization""" - # ! ---- Operators ---- + # ! --- DOF management --- - self.div = darsia.FVDivergence(self.grid).mat - """sps.csc_matrix: divergence operator: flat fluxes -> flat potentials""" - - self.mass_matrix_cells = darsia.FVMass(self.grid).mat - """sps.csc_matrix: mass matrix on cells: flat potentials -> flat potentials""" - - lumping = self.options.get("lumping", True) - self.mass_matrix_faces = darsia.FVMass(self.grid, "faces", lumping).mat - """sps.csc_matrix: mass matrix on faces: flat fluxes -> flat fluxes""" + # The following degrees of freedom are considered (also in this order): + # - flat fluxes (normal fluxes on the faces) + # - flat potentials (potentials on the cells) + # - lagrange multiplier (scalar variable) + # Idea: Fix the potential in the center of the domain to zero. This is done by + # adding a constraint to the potential via a Lagrange multiplier. - self.orthogonal_face_average = darsia.FVFaceAverage(self.grid).mat - """sps.csc_matrix: averaging operator for fluxes on orthogonal faces""" + num_flux_dofs = self.grid.num_faces + num_potential_dofs = self.grid.num_cells + num_lagrange_multiplier_dofs = 1 + num_dofs = ( + num_flux_dofs + num_potential_dofs + num_lagrange_multiplier_dofs + ) # total number of dofs - # Define sparse embedding operators, and quick access through indices. - # Assume the ordering of the faces is vertical faces first, then horizontal - # faces. After that, cell variabes are provided for the potential, finally a - # scalar variable for the lagrange multiplier. - self.flux_embedding = sps.csc_matrix( - ( - np.ones(self.grid.num_faces, dtype=float), - (np.arange(self.grid.num_faces), np.arange(self.grid.num_faces)), - ), - shape=(self.grid.num_faces + self.grid.num_cells + 1, self.grid.num_faces), - ) - - self.flux_indices = np.arange(self.grid.num_faces) + self.flux_indices = np.arange(num_flux_dofs) self.potential_indices = np.arange( - self.grid.num_faces, self.grid.num_faces + self.grid.num_cells + num_flux_dofs, num_flux_dofs + num_potential_dofs ) self.lagrange_multiplier_indices = np.array( - [self.grid.num_faces + self.grid.num_cells], dtype=int + [num_flux_dofs + num_potential_dofs], dtype=int ) - # ! ---- Utilities ---- - aa_depth = self.options.get("aa_depth", 0) - aa_restart = self.options.get("aa_restart", None) - self.anderson = ( - darsia.AndersonAcceleration( - dimension=None, depth=aa_depth, restart=aa_restart - ) - if aa_depth > 0 - else None + # ! --- Embedding operators --- + + self.flux_embedding = sps.csc_matrix( + ( + np.ones(num_flux_dofs, dtype=float), + (self.flux_indices, self.flux_indices), + ), + shape=(num_dofs, num_flux_dofs), ) + """sps.csc_matrix: embedding operator for fluxes""" - def _problem_specific_setup(self, mass_diff: np.ndarray) -> None: - """Resetup of fixed discretization""" + # ! ---- Constraint for the potential correpsonding to Lagrange multiplier ---- - # Fix index of center cell center = 0.5 * np.array(self.grid.shape) center_cell = center.astype(int) self.constrained_cell_flat_index = np.ravel_multi_index( @@ -139,11 +124,27 @@ def _problem_specific_setup(self, mass_diff: np.ndarray) -> None: np.ones(1, dtype=float), (np.zeros(1, dtype=int), np.array([self.constrained_cell_flat_index])), ), - shape=(1, self.grid.num_cells), + shape=(1, num_potential_dofs), dtype=float, ) + """sps.csc_matrix: effective constraint for the potential""" + + # ! ---- Discretization operators ---- + + self.div = darsia.FVDivergence(self.grid).mat + """sps.csc_matrix: divergence operator: flat fluxes -> flat potentials""" + + self.mass_matrix_cells = darsia.FVMass(self.grid).mat + """sps.csc_matrix: mass matrix on cells: flat potentials -> flat potentials""" + + lumping = self.options.get("lumping", True) + self.mass_matrix_faces = darsia.FVMass(self.grid, "faces", lumping).mat + """sps.csc_matrix: mass matrix on faces: flat fluxes -> flat fluxes""" + + self.orthogonal_face_average = darsia.FVFaceAverage(self.grid).mat + """sps.csc_matrix: averaging operator for fluxes on orthogonal faces""" - # Linear part of the operator. + # Linear part of the Darcy operator with potential constraint. self.broken_darcy = sps.bmat( [ [None, -self.div.T, None], @@ -152,6 +153,19 @@ def _problem_specific_setup(self, mass_diff: np.ndarray) -> None: ], format="csc", ) + """sps.csc_matrix: linear part of the Darcy operator""" + + # ! ---- Acceleration ---- + aa_depth = self.options.get("aa_depth", 0) + aa_restart = self.options.get("aa_restart", None) + self.anderson = ( + darsia.AndersonAcceleration( + dimension=None, depth=aa_depth, restart=aa_restart + ) + if aa_depth > 0 + else None + ) + """darsia.AndersonAcceleration: Anderson acceleration""" def split_solution( self, solution: np.ndarray @@ -414,7 +428,6 @@ def __call__( # Determine difference of distriutions and define corresponding rhs mass_diff = img_1.img - img_2.img flat_mass_diff = np.ravel(mass_diff) - self._problem_specific_setup(mass_diff) # Main method distance, solution, status = self._solve(flat_mass_diff) @@ -1160,9 +1173,11 @@ def _solve(self, flat_mass_diff: np.ndarray) -> tuple[float, np.ndarray, dict]: class WassersteinDistanceBregman(VariationalWassersteinDistance): - def _problem_specific_setup(self, mass_diff: np.ndarray) -> None: - super()._problem_specific_setup(mass_diff) + def _setup(self) -> None: + """Setup the problem.""" + super()._setup() self.L = self.options.get("L", 1.0) + """Penality parameter for the Bregman iteration.""" # TODO almost as for Newton. Unify! diff --git a/src/darsia/utils/grid.py b/src/darsia/utils/grid.py index 814c09b2..c137bea3 100644 --- a/src/darsia/utils/grid.py +++ b/src/darsia/utils/grid.py @@ -3,7 +3,6 @@ from typing import Union import numpy as np -import scipy.sparse as sps import darsia @@ -112,10 +111,8 @@ def _setup(self) -> None: # Info about inner cells # TODO rm? - inner_cells_with_vertical_faces = np.ravel(numbering_cells[:, 1:-1]) - inner_cells_with_horizontal_faces = np.ravel(numbering_cells[1:-1, :]) - num_inner_cells_with_vertical_faces = len(inner_cells_with_vertical_faces) - num_inner_cells_with_horizontal_faces = len(inner_cells_with_horizontal_faces) + self.inner_cells_with_vertical_faces = np.ravel(numbering_cells[:, 1:-1]) + self.inner_cells_with_horizontal_faces = np.ravel(numbering_cells[1:-1, :]) # ! ---- Cache ---- # TODO reduce From c1d8052bf238c443556c27263bd4bd8bf4110c83 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Sun, 17 Sep 2023 11:28:42 +0200 Subject: [PATCH 057/100] MAINT: basic maintanence --- src/darsia/measure/wasserstein.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index 4c61bd67..60b2ab07 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -114,8 +114,7 @@ def _setup(self) -> None: # ! ---- Constraint for the potential correpsonding to Lagrange multiplier ---- - center = 0.5 * np.array(self.grid.shape) - center_cell = center.astype(int) + center_cell = np.array(self.grid.shape) // 2 self.constrained_cell_flat_index = np.ravel_multi_index( center_cell, self.grid.shape ) @@ -1015,11 +1014,13 @@ def _solve(self, flat_mass_diff: np.ndarray) -> tuple[float, np.ndarray, dict]: # and therefore better solutions to mu and u. Higher depth is better, but more # expensive. + self.L = self.options.get("L", 1.0) + """float: relaxation parameter, lower cut-off for the mobility""" + # Setup tic = time.time() self.setup_infrastructure() time_infrastructure = time.time() - tic - print("timing infra structure", time_infrastructure) # Solver parameters num_iter = self.options.get("num_iter", 100) @@ -1027,9 +1028,6 @@ def _solve(self, flat_mass_diff: np.ndarray) -> tuple[float, np.ndarray, dict]: tol_increment = self.options.get("tol_increment", 1e-6) tol_distance = self.options.get("tol_distance", 1e-6) - # Relaxation parameter - self.L = self.options.get("L", 1.0) - # Define right hand side rhs = np.concatenate( [ From 3a752a4a65df5112f88419f9964f86df839bea96 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Sun, 17 Sep 2023 22:45:38 +0200 Subject: [PATCH 058/100] MAINT: Replace hardcoded slices. --- src/darsia/measure/wasserstein.py | 109 ++++++++++++++++-------------- 1 file changed, 60 insertions(+), 49 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index 60b2ab07..d0752e34 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -77,7 +77,7 @@ def __init__( def _setup(self) -> None: """Setup of fixed discretization""" - # ! --- DOF management --- + # ! ---- DOF management ---- # The following degrees of freedom are considered (also in this order): # - flat fluxes (normal fluxes on the faces) @@ -101,6 +101,14 @@ def _setup(self) -> None: [num_flux_dofs + num_potential_dofs], dtype=int ) + self.flux_slice = slice(0, num_flux_dofs) + self.potential_slice = slice(num_flux_dofs, num_flux_dofs + num_potential_dofs) + self.lagrange_multiplier_slice = slice( + num_flux_dofs + num_potential_dofs, + num_flux_dofs + num_potential_dofs + num_lagrange_multiplier_dofs, + ) + self.reduced_system_slice = slice(num_flux_dofs, None) + # ! --- Embedding operators --- self.flux_embedding = sps.csc_matrix( @@ -662,8 +670,8 @@ def setup_infrastructure(self) -> None: # Step 2: Remove flux blocks through Schur complement approach # Build Schur complement wrt. flux-flux block - J_inv = sps.diags(1.0 / jacobian.diagonal()[self.flux_indices]) - D = jacobian[self.grid.num_faces :, : self.grid.num_faces].copy() + J_inv = sps.diags(1.0 / jacobian.diagonal()[self.flux_slice]) + D = jacobian[self.reduced_system_slice, self.flux_slice].copy() schur_complement = D.dot(J_inv.dot(D.T)) # Cache divergence matrix @@ -672,7 +680,7 @@ def setup_infrastructure(self) -> None: # Cache (constant) jacobian subblock self.jacobian_subblock = jacobian[ - self.grid.num_faces :, self.grid.num_faces : + self.reduced_system_slice, self.reduced_system_slice ].copy() # Add Schur complement - use this to identify sparsity structure @@ -778,15 +786,15 @@ def remove_flux(self, jacobian: sps.csc_matrix, residual: np.ndarray) -> tuple: """ # Build Schur complement wrt flux-block - J_inv = sps.diags(1.0 / jacobian.diagonal()[self.flux_indices]) + J_inv = sps.diags(1.0 / jacobian.diagonal()[self.flux_slice]) schur_complement = self.D.dot(J_inv.dot(self.DT)) # Gauss eliminiation on matrices reduced_jacobian = self.jacobian_subblock + schur_complement # Gauss elimination on vectors - reduced_residual = residual[self.reduced_system_indices].copy() - reduced_residual -= self.D.dot(J_inv.dot(residual[self.flux_indices])) + reduced_residual = residual[self.reduced_system_slice].copy() + reduced_residual -= self.D.dot(J_inv.dot(residual[self.flux_slice])) return reduced_jacobian, reduced_residual, J_inv @@ -918,7 +926,7 @@ def linearization_step( lu = sps.linalg.splu(self.reduced_jacobian) time_setup = time.time() - tic tic = time.time() - update[self.reduced_system_indices] = lu.solve(self.reduced_residual) + update[self.reduced_system_slice] = lu.solve(self.reduced_residual) elif linear_solver == "amg-flux-reduced": ml = pyamg.smoothed_aggregation_solver( @@ -926,16 +934,16 @@ def linearization_step( ) time_setup = time.time() - tic tic = time.time() - update[self.reduced_system_indices] = ml.solve( + update[self.reduced_system_slice] = ml.solve( self.reduced_residual, tol=tol_amg, residuals=res_history_amg, ) # Compute flux update - update[self.flux_indices] = jacobian_flux_inv.dot( - residual[self.flux_indices] - + self.DT.dot(update[self.reduced_system_indices]) + update[self.flux_slice] = jacobian_flux_inv.dot( + residual[self.flux_slice] + + self.DT.dot(update[self.reduced_system_slice]) ) time_solve = time.time() - tic @@ -979,9 +987,9 @@ def linearization_step( ) # Compute flux update - update[self.flux_indices] = jacobian_flux_inv.dot( - residual[self.flux_indices] - + self.DT.dot(update[self.reduced_system_indices]) + update[self.flux_slice] = jacobian_flux_inv.dot( + residual[self.flux_slice] + + self.DT.dot(update[self.reduced_system_slice]) ) time_solve = time.time() - tic @@ -1109,15 +1117,15 @@ def _solve(self, flat_mass_diff: np.ndarray) -> tuple[float, np.ndarray, dict]: error = [ np.linalg.norm(residual_i, 2), [ - np.linalg.norm(residual_i[: self.grid.num_faces], 2), - np.linalg.norm(residual_i[self.grid.num_faces : -1], 2), - np.linalg.norm(residual_i[-1:], 2), + np.linalg.norm(residual_i[self.flux_slice], 2), + np.linalg.norm(residual_i[self.potential_slice], 2), + np.linalg.norm(residual_i[self.lagrange_multiplier_slice], 2), ], np.linalg.norm(increment, 2), [ - np.linalg.norm(increment[: self.grid.num_faces], 2), - np.linalg.norm(increment[self.grid.num_faces : -1], 2), - np.linalg.norm(increment[-1:], 2), + np.linalg.norm(increment[self.flux_slice], 2), + np.linalg.norm(increment[self.potential_slice], 2), + np.linalg.norm(increment[self.lagrange_multiplier_slice], 2), ], abs(new_distance - old_distance), ] @@ -1177,6 +1185,9 @@ def _setup(self) -> None: self.L = self.options.get("L", 1.0) """Penality parameter for the Bregman iteration.""" + self.force_slice = slice(self.grid.num_faces, None) + """slice: slice for the force.""" + # TODO almost as for Newton. Unify! def darcy_jacobian(self) -> sps.linalg.LinearOperator: @@ -1213,8 +1224,8 @@ def setup_infrastructure(self) -> None: # Step 2: Remove flux blocks through Schur complement approach # Build Schur complement wrt. flux-flux block - J_inv = sps.diags(1.0 / jacobian.diagonal()[self.flux_indices]) - D = jacobian[self.grid.num_faces :, : self.grid.num_faces].copy() + J_inv = sps.diags(1.0 / jacobian.diagonal()[self.flux_slice]) + D = jacobian[self.reduced_system_slice, self.flux_slice].copy() schur_complement = D.dot(J_inv.dot(D.T)) # Cache divergence matrix @@ -1329,15 +1340,15 @@ def remove_flux(self, jacobian: sps.csc_matrix, residual: np.ndarray) -> tuple: """ # Build Schur complement wrt flux-block - J_inv = sps.diags(1.0 / jacobian.diagonal()[self.flux_indices]) + J_inv = sps.diags(1.0 / jacobian.diagonal()[self.flux_slice]) schur_complement = self.D.dot(J_inv.dot(self.DT)) # Gauss eliminiation on matrices reduced_jacobian = self.jacobian_subblock + schur_complement # Gauss elimination on vectors - reduced_residual = residual[self.reduced_system_indices].copy() - reduced_residual -= self.D.dot(J_inv.dot(residual[self.flux_indices])) + reduced_residual = residual[self.reduced_system_slice].copy() + reduced_residual -= self.D.dot(J_inv.dot(residual[self.flux_slice])) return reduced_jacobian, reduced_residual, J_inv @@ -1551,7 +1562,7 @@ def _solve(self, flat_mass_diff): self.reduced_jacobian, **ml_options ) solution_i[ - self.reduced_system_indices + self.reduced_system_slice ] = self.l_scheme_mixed_darcy_solver.solve( self.reduced_residual, tol=tol_amg, @@ -1559,9 +1570,9 @@ def _solve(self, flat_mass_diff): ) # Compute flux update - solution_i[self.flux_indices] = jacobian_flux_inv.dot( - rhs[self.flux_indices] - + self.DT.dot(solution_i[self.reduced_system_indices]) + solution_i[self.flux_slice] = jacobian_flux_inv.dot( + rhs[self.flux_slice] + + self.DT.dot(solution_i[self.reduced_system_slice]) ) elif linear_solver in ["lu-potential", "amg-potential"]: @@ -1603,9 +1614,9 @@ def _solve(self, flat_mass_diff): ) # Compute flux update - solution_i[self.flux_indices] = jacobian_flux_inv.dot( - rhs[self.flux_indices] - + self.DT.dot(solution_i[self.reduced_system_indices]) + solution_i[self.flux_slice] = jacobian_flux_inv.dot( + rhs[self.flux_slice] + + self.DT.dot(solution_i[self.reduced_system_slice]) ) else: @@ -1625,7 +1636,7 @@ def _solve(self, flat_mass_diff): # 1. Make relaxation step (solve quadratic optimization problem) tic = time.time() rhs_i = rhs.copy() - rhs_i[: self.grid.num_faces] = self.L * self.mass_matrix_faces.dot( + rhs_i[self.flux_slice] = self.L * self.mass_matrix_faces.dot( old_aux_flux - old_force ) new_flux, _, _ = self.split_solution( @@ -1653,8 +1664,8 @@ def _solve(self, flat_mass_diff): inc = np.concatenate([aux_inc, force_inc]) iteration = np.concatenate([new_aux_flux, new_force]) new_iteration = self.anderson(iteration, inc, iter) - new_aux_flux = new_iteration[: self.grid.num_faces] - new_force = new_iteration[self.grid.num_faces :] + new_aux_flux = new_iteration[self.flux_slice] + new_force = new_iteration[self.force_slice] toc = time.time() time_anderson = toc - tic @@ -1694,8 +1705,8 @@ def _solve(self, flat_mass_diff): iteration = np.concatenate([new_flux, new_force]) # new_flux = self.anderson(new_flux, flux_inc, iter) new_iteration = self.anderson(iteration, inc, iter) - new_flux = new_iteration[: self.grid.num_faces] - new_force = new_iteration[self.grid.num_faces :] + new_flux = new_iteration[self.flux_slice] + new_force = new_iteration[self.force_slice] toc = time.time() time_anderson = toc - tic @@ -1758,7 +1769,7 @@ def _solve(self, flat_mass_diff): self.reduced_jacobian ) solution_i[ - self.reduced_system_indices + self.reduced_system_slice ] = self.l_scheme_mixed_darcy_solver.solve( self.reduced_residual ) @@ -1771,7 +1782,7 @@ def _solve(self, flat_mass_diff): ) ) solution_i[ - self.reduced_system_indices + self.reduced_system_slice ] = self.l_scheme_mixed_darcy_solver.solve( self.reduced_residual, tol=tol_amg, @@ -1779,9 +1790,9 @@ def _solve(self, flat_mass_diff): ) # Compute flux update - solution_i[self.flux_indices] = jacobian_flux_inv.dot( - rhs_i[self.flux_indices] - + self.DT.dot(solution_i[self.reduced_system_indices]) + solution_i[self.flux_slice] = jacobian_flux_inv.dot( + rhs_i[self.flux_slice] + + self.DT.dot(solution_i[self.reduced_system_slice]) ) elif linear_solver in ["lu-potential", "amg-potential"]: @@ -1831,9 +1842,9 @@ def _solve(self, flat_mass_diff): ) # Compute flux update - solution_i[self.flux_indices] = jacobian_flux_inv.dot( - rhs_i[self.flux_indices] - + self.DT.dot(solution_i[self.reduced_system_indices]) + solution_i[self.flux_slice] = jacobian_flux_inv.dot( + rhs_i[self.flux_slice] + + self.DT.dot(solution_i[self.reduced_system_slice]) ) else: raise NotImplementedError( @@ -1874,8 +1885,8 @@ def _solve(self, flat_mass_diff): inc = np.concatenate([aux_inc, force_inc]) iteration = np.concatenate([new_aux_flux, new_force]) new_iteration = self.anderson(iteration, inc, iter) - new_aux_flux = new_iteration[: self.grid.num_faces] - new_force = new_iteration[self.grid.num_faces :] + new_aux_flux = new_iteration[self.flux_slice] + new_force = new_iteration[self.force_slice] toc = time.time() time_anderson = toc - tic @@ -1891,7 +1902,7 @@ def _solve(self, flat_mass_diff): # Determine the error in the mass conservation equation mass_conservation_residual = np.linalg.norm( - self.div.dot(new_flux) - rhs[self.grid.num_faces : -1], 2 + self.div.dot(new_flux) - rhs[self.potential_slice], 2 ) # Determine increments From 935e325168a8e7f8f4b0668a2705666a188cc748 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Sun, 17 Sep 2023 23:01:28 +0200 Subject: [PATCH 059/100] MAINT: Reuse code --- src/darsia/measure/wasserstein.py | 698 +++++++++++------------------- 1 file changed, 246 insertions(+), 452 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index d0752e34..0bc97c2c 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -83,6 +83,7 @@ def _setup(self) -> None: # - flat fluxes (normal fluxes on the faces) # - flat potentials (potentials on the cells) # - lagrange multiplier (scalar variable) + # Idea: Fix the potential in the center of the domain to zero. This is done by # adding a constraint to the potential via a Lagrange multiplier. @@ -162,6 +163,17 @@ def _setup(self) -> None: ) """sps.csc_matrix: linear part of the Darcy operator""" + L_init = self.options.get("L_init", 1.0) + self.darcy_init = sps.bmat( + [ + [L_init * self.mass_matrix_faces, -self.div.T, None], + [self.div, None, -self.potential_constraint.T], + [None, self.potential_constraint, None], + ], + format="csc", + ) + """sps.csc_matrix: initial Darcy operator""" + # ! ---- Acceleration ---- aa_depth = self.options.get("aa_depth", 0) aa_restart = self.options.get("aa_restart", None) @@ -187,11 +199,9 @@ def split_solution( """ # Split the solution - flat_flux = solution[: self.grid.num_faces] - flat_potential = solution[ - self.grid.num_faces : self.grid.num_faces + self.grid.num_cells - ] - flat_lagrange_multiplier = solution[-1] + flat_flux = solution[self.flux_slice] + flat_potential = solution[self.potential_slice] + flat_lagrange_multiplier = solution[self.lagrange_multiplier_slice] return flat_flux, flat_potential, flat_lagrange_multiplier @@ -401,6 +411,185 @@ def vector_face_flux_norm(self, flat_flux: np.ndarray, mode: str) -> np.ndarray: return flat_flux_norm + # ! ---- Solver methods ---- + + def setup_infrastructure(self) -> None: + """Setup the infrastructure for reduced systems through Gauss elimination. + + Provide internal data structures for the reduced system. + + """ + # Step 1: Compute the jacobian of the Darcy problem + + # The Darcy problem is sufficient + jacobian = self.darcy_init.copy() + + # Step 2: Remove flux blocks through Schur complement approach + + # Build Schur complement wrt. flux-flux block + J_inv = sps.diags(1.0 / jacobian.diagonal()[self.flux_slice]) + D = jacobian[self.reduced_system_slice, self.flux_slice].copy() + schur_complement = D.dot(J_inv.dot(D.T)) + + # Cache divergence matrix + self.D = D.copy() + self.DT = self.D.T.copy() + + # Cache (constant) jacobian subblock + self.jacobian_subblock = jacobian[ + self.reduced_system_slice, self.reduced_system_slice + ].copy() + + # Add Schur complement - use this to identify sparsity structure + # Cache the reduced jacobian + self.reduced_jacobian = self.jacobian_subblock + schur_complement + + # Step 3: Remove potential block through Gauss elimination + + # Find row entries to be removed + rm_row_entries = np.arange( + self.reduced_jacobian.indptr[self.constrained_cell_flat_index], + self.reduced_jacobian.indptr[self.constrained_cell_flat_index + 1], + ) + + # Find column entries to be removed + rm_col_entries = np.where( + self.reduced_jacobian.indices == self.constrained_cell_flat_index + )[0] + + # Collect all entries to be removes + rm_indices = np.unique( + np.concatenate((rm_row_entries, rm_col_entries)).astype(int) + ) + # Cache for later use in remove_lagrange_multiplier + self.rm_indices = rm_indices + + # Identify rows to be reduced + rm_rows = [ + np.max(np.where(self.reduced_jacobian.indptr <= index)[0]) + for index in rm_indices + ] + + # Reduce data - simply remove + fully_reduced_jacobian_data = np.delete(self.reduced_jacobian.data, rm_indices) + + # Reduce indices - remove and shift + fully_reduced_jacobian_indices = np.delete( + self.reduced_jacobian.indices, rm_indices + ) + fully_reduced_jacobian_indices[ + fully_reduced_jacobian_indices > self.constrained_cell_flat_index + ] -= 1 + + # Reduce indptr - shift and remove + # NOTE: As only a few entries should be removed, this is not too expensive + # and a for loop is used + fully_reduced_jacobian_indptr = self.reduced_jacobian.indptr.copy() + for row in rm_rows: + fully_reduced_jacobian_indptr[row + 1 :] -= 1 + fully_reduced_jacobian_indptr = np.unique(fully_reduced_jacobian_indptr) + + # Make sure two rows are removed and deduce shape of reduced jacobian + assert ( + len(fully_reduced_jacobian_indptr) == len(self.reduced_jacobian.indptr) - 2 + ), "Two rows should be removed." + fully_reduced_jacobian_shape = ( + len(fully_reduced_jacobian_indptr) - 1, + len(fully_reduced_jacobian_indptr) - 1, + ) + + # Cache the fully reduced jacobian + self.fully_reduced_jacobian = sps.csc_matrix( + ( + fully_reduced_jacobian_data, + fully_reduced_jacobian_indices, + fully_reduced_jacobian_indptr, + ), + shape=fully_reduced_jacobian_shape, + ) + + # Cache the indices and indptr + self.fully_reduced_jacobian_indices = fully_reduced_jacobian_indices.copy() + self.fully_reduced_jacobian_indptr = fully_reduced_jacobian_indptr.copy() + self.fully_reduced_jacobian_shape = fully_reduced_jacobian_shape + + # Step 4: Identify inclusions (index arrays) + + # Define reduced system indices wrt full system + reduced_system_indices = np.concatenate( + [self.potential_indices, self.lagrange_multiplier_indices] + ) + + # Define fully reduced system indices wrt reduced system - need to remove cell + # (and implicitly lagrange multiplier) + self.fully_reduced_system_indices = np.delete( + np.arange(self.grid.num_cells), self.constrained_cell_flat_index + ) + + # Define fully reduced system indices wrt full system + self.fully_reduced_system_indices_full = reduced_system_indices[ + self.fully_reduced_system_indices + ] + + def remove_flux(self, jacobian: sps.csc_matrix, residual: np.ndarray) -> tuple: + """Remove the flux block from the jacobian and residual. + + Args: + jacobian (sps.csc_matrix): jacobian + residual (np.ndarray): residual + + Returns: + tuple: reduced jacobian, reduced residual, inverse of flux block + + """ + # Build Schur complement wrt flux-block + J_inv = sps.diags(1.0 / jacobian.diagonal()[self.flux_slice]) + schur_complement = self.D.dot(J_inv.dot(self.DT)) + + # Gauss eliminiation on matrices + reduced_jacobian = self.jacobian_subblock + schur_complement + + # Gauss elimination on vectors + reduced_residual = residual[self.reduced_system_slice].copy() + reduced_residual -= self.D.dot(J_inv.dot(residual[self.flux_slice])) + + return reduced_jacobian, reduced_residual, J_inv + + def remove_lagrange_multiplier(self, jacobian, residual, solution) -> tuple: + """Shortcut for removing the lagrange multiplier from the reduced jacobian. + + Args: + + solution (np.ndarray): solution, TODO make function independent of solution + + Returns: + tuple: fully reduced jacobian, fully reduced residual + + """ + # Make sure the jacobian is a CSC matrix + assert isinstance(jacobian, sps.csc_matrix), "Jacobian should be a CSC matrix." + + # Effective Gauss-elimination for the particular case of the lagrange multiplier + self.fully_reduced_jacobian.data[:] = np.delete( + self.reduced_jacobian.data.copy(), self.rm_indices + ) + # NOTE: The indices have to be restored if the LU factorization is to be used + # FIXME omit if not required + self.fully_reduced_jacobian.indices = self.fully_reduced_jacobian_indices.copy() + + # Rhs is not affected by Gauss elimination as it is assumed that the residual + # is zero in the constrained cell, and the pressure is zero there as well. + # If not, we need to do a proper Gauss elimination on the right hand side! + if abs(residual[-1]) > 1e-6: + raise NotImplementedError("Implementation requires residual to be zero.") + if abs(solution[self.grid.num_faces + self.constrained_cell_flat_index]) > 1e-6: + raise NotImplementedError("Implementation requires solution to be zero.") + fully_reduced_residual = self.reduced_residual[ + self.fully_reduced_system_indices + ].copy() + + return self.fully_reduced_jacobian, fully_reduced_residual + # ! ---- Main methods ---- def __call__( @@ -581,257 +770,62 @@ def _plot_solution( plt.close("all") -class WassersteinDistanceNewton(VariationalWassersteinDistance): - """Class to determine the L1 EMD/Wasserstein distance solved with Newton's method.""" - - def residual(self, rhs: np.ndarray, solution: np.ndarray) -> np.ndarray: - """Compute the residual of the solution. - - Args: - rhs (np.ndarray): right hand side - solution (np.ndarray): solution - - Returns: - np.ndarray: residual - - """ - flat_flux, _, _ = self.split_solution(solution) - mode = self.options.get("mode", "face_arithmetic") - flat_flux_norm = np.maximum( - self.vector_face_flux_norm(flat_flux, mode=mode), self.regularization - ) - flat_flux_normed = flat_flux / flat_flux_norm - - return ( - rhs - - self.broken_darcy.dot(solution) - - self.flux_embedding.dot(self.mass_matrix_faces.dot(flat_flux_normed)) - ) - - def jacobian(self, solution: np.ndarray) -> sps.linalg.LinearOperator: - """Compute the LU factorization of the jacobian of the solution. - - Args: - solution (np.ndarray): solution - - Returns: - sps.linalg.splu: LU factorization of the jacobian - - """ - flat_flux, _, _ = self.split_solution(solution) - mode = self.options.get("mode", "face_arithmetic") - flat_flux_norm = np.maximum( - self.vector_face_flux_norm(flat_flux, mode=mode), self.regularization - ) - approx_jacobian = sps.bmat( - [ - [ - sps.diags(np.maximum(self.L, 1.0 / flat_flux_norm), dtype=float) - * self.mass_matrix_faces, - -self.div.T, - None, - ], - [self.div, None, -self.potential_constraint.T], - [None, self.potential_constraint, None], - ], - format="csc", - ) - return approx_jacobian - - def darcy_jacobian(self) -> sps.linalg.LinearOperator: - """Compute the Jacobian of a standard homogeneous Darcy problem. - - The mobility is defined by the used via options["L_init"]. The jacobian is - cached for later use. - - """ - L_init = self.options.get("L_init", 1.0) - jacobian = sps.bmat( - [ - [L_init * self.mass_matrix_faces, -self.div.T, None], - [self.div, None, -self.potential_constraint.T], - [None, self.potential_constraint, None], - ], - format="csc", - ) - return jacobian - - def setup_infrastructure(self) -> None: - """Setup the infrastructure for reduced systems through Gauss elimination. - - Provide internal data structures for the reduced system. - - """ - # Step 1: Compute the jacobian of the Darcy problem - - # The Darcy problem is sufficient - jacobian = self.darcy_jacobian() - - # Step 2: Remove flux blocks through Schur complement approach - - # Build Schur complement wrt. flux-flux block - J_inv = sps.diags(1.0 / jacobian.diagonal()[self.flux_slice]) - D = jacobian[self.reduced_system_slice, self.flux_slice].copy() - schur_complement = D.dot(J_inv.dot(D.T)) - - # Cache divergence matrix - self.D = D.copy() - self.DT = self.D.T.copy() - - # Cache (constant) jacobian subblock - self.jacobian_subblock = jacobian[ - self.reduced_system_slice, self.reduced_system_slice - ].copy() - - # Add Schur complement - use this to identify sparsity structure - # Cache the reduced jacobian - self.reduced_jacobian = self.jacobian_subblock + schur_complement - - # Step 3: Remove potential block through Gauss elimination - - # Find row entries to be removed - rm_row_entries = np.arange( - self.reduced_jacobian.indptr[self.constrained_cell_flat_index], - self.reduced_jacobian.indptr[self.constrained_cell_flat_index + 1], - ) - - # Find column entries to be removed - rm_col_entries = np.where( - self.reduced_jacobian.indices == self.constrained_cell_flat_index - )[0] - - # Collect all entries to be removes - rm_indices = np.unique( - np.concatenate((rm_row_entries, rm_col_entries)).astype(int) - ) - # Cache for later use in remove_lagrange_multiplier - self.rm_indices = rm_indices - - # Identify rows to be reduced - rm_rows = [ - np.max(np.where(self.reduced_jacobian.indptr <= index)[0]) - for index in rm_indices - ] - - # Reduce data - simply remove - fully_reduced_jacobian_data = np.delete(self.reduced_jacobian.data, rm_indices) - - # Reduce indices - remove and shift - fully_reduced_jacobian_indices = np.delete( - self.reduced_jacobian.indices, rm_indices - ) - fully_reduced_jacobian_indices[ - fully_reduced_jacobian_indices > self.constrained_cell_flat_index - ] -= 1 - - # Reduce indptr - shift and remove - # NOTE: As only a few entries should be removed, this is not too expensive - # and a for loop is used - fully_reduced_jacobian_indptr = self.reduced_jacobian.indptr.copy() - for row in rm_rows: - fully_reduced_jacobian_indptr[row + 1 :] -= 1 - fully_reduced_jacobian_indptr = np.unique(fully_reduced_jacobian_indptr) - - # Make sure two rows are removed and deduce shape of reduced jacobian - assert ( - len(fully_reduced_jacobian_indptr) == len(self.reduced_jacobian.indptr) - 2 - ), "Two rows should be removed." - fully_reduced_jacobian_shape = ( - len(fully_reduced_jacobian_indptr) - 1, - len(fully_reduced_jacobian_indptr) - 1, - ) - - # Cache the fully reduced jacobian - self.fully_reduced_jacobian = sps.csc_matrix( - ( - fully_reduced_jacobian_data, - fully_reduced_jacobian_indices, - fully_reduced_jacobian_indptr, - ), - shape=fully_reduced_jacobian_shape, - ) - - # Cache the indices and indptr - self.fully_reduced_jacobian_indices = fully_reduced_jacobian_indices.copy() - self.fully_reduced_jacobian_indptr = fully_reduced_jacobian_indptr.copy() - self.fully_reduced_jacobian_shape = fully_reduced_jacobian_shape - - # Step 4: Identify inclusions (index arrays) - - # Define reduced system indices wrt full system - self.reduced_system_indices = np.concatenate( - [self.potential_indices, self.lagrange_multiplier_indices] - ) - - # Define fully reduced system indices wrt reduced system - need to remove cell - # (and implicitly lagrange multiplier) - self.fully_reduced_system_indices = np.delete( - np.arange(self.grid.num_cells), self.constrained_cell_flat_index - ) - - # Define fully reduced system indices wrt full system - self.fully_reduced_system_indices_full = self.reduced_system_indices[ - self.fully_reduced_system_indices - ] - - def remove_flux(self, jacobian: sps.csc_matrix, residual: np.ndarray) -> tuple: - """Remove the flux block from the jacobian and residual. - - Args: - jacobian (sps.csc_matrix): jacobian - residual (np.ndarray): residual - - Returns: - tuple: reduced jacobian, reduced residual, inverse of flux block - - """ - # Build Schur complement wrt flux-block - J_inv = sps.diags(1.0 / jacobian.diagonal()[self.flux_slice]) - schur_complement = self.D.dot(J_inv.dot(self.DT)) - - # Gauss eliminiation on matrices - reduced_jacobian = self.jacobian_subblock + schur_complement - - # Gauss elimination on vectors - reduced_residual = residual[self.reduced_system_slice].copy() - reduced_residual -= self.D.dot(J_inv.dot(residual[self.flux_slice])) - - return reduced_jacobian, reduced_residual, J_inv +class WassersteinDistanceNewton(VariationalWassersteinDistance): + """Class to determine the L1 EMD/Wasserstein distance solved with Newton's method.""" - def remove_lagrange_multiplier(self, jacobian, residual, solution) -> tuple: - """Shortcut for removing the lagrange multiplier from the reduced jacobian. + def residual(self, rhs: np.ndarray, solution: np.ndarray) -> np.ndarray: + """Compute the residual of the solution. Args: - - solution (np.ndarray): solution, TODO make function independent of solution + rhs (np.ndarray): right hand side + solution (np.ndarray): solution Returns: - tuple: fully reduced jacobian, fully reduced residual + np.ndarray: residual """ - # Make sure the jacobian is a CSC matrix - assert isinstance(jacobian, sps.csc_matrix), "Jacobian should be a CSC matrix." + flat_flux, _, _ = self.split_solution(solution) + mode = self.options.get("mode", "face_arithmetic") + flat_flux_norm = np.maximum( + self.vector_face_flux_norm(flat_flux, mode=mode), self.regularization + ) + flat_flux_normed = flat_flux / flat_flux_norm - # Effective Gauss-elimination for the particular case of the lagrange multiplier - self.fully_reduced_jacobian.data[:] = np.delete( - self.reduced_jacobian.data.copy(), self.rm_indices + return ( + rhs + - self.broken_darcy.dot(solution) + - self.flux_embedding.dot(self.mass_matrix_faces.dot(flat_flux_normed)) ) - # NOTE: The indices have to be restored if the LU factorization is to be used - # FIXME omit if not required - self.fully_reduced_jacobian.indices = self.fully_reduced_jacobian_indices.copy() - # Rhs is not affected by Gauss elimination as it is assumed that the residual - # is zero in the constrained cell, and the pressure is zero there as well. - # If not, we need to do a proper Gauss elimination on the right hand side! - if abs(residual[-1]) > 1e-6: - raise NotImplementedError("Implementation requires residual to be zero.") - if abs(solution[self.grid.num_faces + self.constrained_cell_flat_index]) > 1e-6: - raise NotImplementedError("Implementation requires solution to be zero.") - fully_reduced_residual = self.reduced_residual[ - self.fully_reduced_system_indices - ].copy() + def jacobian(self, solution: np.ndarray) -> sps.linalg.LinearOperator: + """Compute the LU factorization of the jacobian of the solution. - return self.fully_reduced_jacobian, fully_reduced_residual + Args: + solution (np.ndarray): solution + + Returns: + sps.linalg.splu: LU factorization of the jacobian + + """ + flat_flux, _, _ = self.split_solution(solution) + mode = self.options.get("mode", "face_arithmetic") + flat_flux_norm = np.maximum( + self.vector_face_flux_norm(flat_flux, mode=mode), self.regularization + ) + approx_jacobian = sps.bmat( + [ + [ + sps.diags(np.maximum(self.L, 1.0 / flat_flux_norm), dtype=float) + * self.mass_matrix_faces, + -self.div.T, + None, + ], + [self.div, None, -self.potential_constraint.T], + [None, self.potential_constraint, None], + ], + format="csc", + ) + return approx_jacobian def linearization_step( self, solution: np.ndarray, rhs: np.ndarray, iter: int @@ -854,7 +848,7 @@ def linearization_step( tic = time.time() if iter == 0: residual = rhs.copy() - approx_jacobian = self.darcy_jacobian() + approx_jacobian = self.darcy_init.copy() else: residual = self.residual(rhs, solution) approx_jacobian = self.jacobian(solution) @@ -1028,7 +1022,6 @@ def _solve(self, flat_mass_diff: np.ndarray) -> tuple[float, np.ndarray, dict]: # Setup tic = time.time() self.setup_infrastructure() - time_infrastructure = time.time() - tic # Solver parameters num_iter = self.options.get("num_iter", 100) @@ -1090,9 +1083,9 @@ def _solve(self, flat_mass_diff: np.ndarray) -> tuple[float, np.ndarray, dict]: # while application to the flux, results in improved performance. tic = time.time() if self.anderson is not None: - solution_i[: self.grid.num_faces] = self.anderson( - solution_i[: self.grid.num_faces], - update_i[: self.grid.num_faces], + solution_i[self.flux_slice] = self.anderson( + solution_i[self.flux_slice], + update_i[self.flux_slice], iter, ) toc = time.time() @@ -1188,205 +1181,6 @@ def _setup(self) -> None: self.force_slice = slice(self.grid.num_faces, None) """slice: slice for the force.""" - # TODO almost as for Newton. Unify! - - def darcy_jacobian(self) -> sps.linalg.LinearOperator: - """Compute the Jacobian of a standard homogeneous Darcy problem. - - The mobility is defined by the used via options["L_init"]. The jacobian is - cached for later use. - - """ - L_init = self.options.get("L", 1.0) # NOTE changed! - jacobian = sps.bmat( - [ - [L_init * self.mass_matrix_faces, -self.div.T, None], - [self.div, None, -self.potential_constraint.T], - [None, self.potential_constraint, None], - ], - format="csc", - ) - return jacobian - - # TODO same as for Newton. Unify! - - def setup_infrastructure(self) -> None: - """Setup the infrastructure for reduced systems through Gauss elimination. - - Provide internal data structures for the reduced system. - - """ - # Step 1: Compute the jacobian of the Darcy problem - - # The Darcy problem is sufficient - jacobian = self.darcy_jacobian() - - # Step 2: Remove flux blocks through Schur complement approach - - # Build Schur complement wrt. flux-flux block - J_inv = sps.diags(1.0 / jacobian.diagonal()[self.flux_slice]) - D = jacobian[self.reduced_system_slice, self.flux_slice].copy() - schur_complement = D.dot(J_inv.dot(D.T)) - - # Cache divergence matrix - self.D = D.copy() - self.DT = self.D.T.copy() - - # Cache (constant) jacobian subblock - self.jacobian_subblock = jacobian[ - self.grid.num_faces :, self.grid.num_faces : - ].copy() - - # Add Schur complement - use this to identify sparsity structure - # Cache the reduced jacobian - self.reduced_jacobian = self.jacobian_subblock + schur_complement - - # Step 3: Remove potential block through Gauss elimination - - # Find row entries to be removed - rm_row_entries = np.arange( - self.reduced_jacobian.indptr[self.constrained_cell_flat_index], - self.reduced_jacobian.indptr[self.constrained_cell_flat_index + 1], - ) - - # Find column entries to be removed - rm_col_entries = np.where( - self.reduced_jacobian.indices == self.constrained_cell_flat_index - )[0] - - # Collect all entries to be removes - rm_indices = np.unique( - np.concatenate((rm_row_entries, rm_col_entries)).astype(int) - ) - # Cache for later use in remove_lagrange_multiplier - self.rm_indices = rm_indices - - # Identify rows to be reduced - rm_rows = [ - np.max(np.where(self.reduced_jacobian.indptr <= index)[0]) - for index in rm_indices - ] - - # Reduce data - simply remove - fully_reduced_jacobian_data = np.delete(self.reduced_jacobian.data, rm_indices) - - # Reduce indices - remove and shift - fully_reduced_jacobian_indices = np.delete( - self.reduced_jacobian.indices, rm_indices - ) - fully_reduced_jacobian_indices[ - fully_reduced_jacobian_indices > self.constrained_cell_flat_index - ] -= 1 - - # Reduce indptr - shift and remove - # NOTE: As only a few entries should be removed, this is not too expensive - # and a for loop is used - fully_reduced_jacobian_indptr = self.reduced_jacobian.indptr.copy() - for row in rm_rows: - fully_reduced_jacobian_indptr[row + 1 :] -= 1 - fully_reduced_jacobian_indptr = np.unique(fully_reduced_jacobian_indptr) - - # Make sure two rows are removed and deduce shape of reduced jacobian - assert ( - len(fully_reduced_jacobian_indptr) == len(self.reduced_jacobian.indptr) - 2 - ), "Two rows should be removed." - fully_reduced_jacobian_shape = ( - len(fully_reduced_jacobian_indptr) - 1, - len(fully_reduced_jacobian_indptr) - 1, - ) - - # Cache the fully reduced jacobian - self.fully_reduced_jacobian = sps.csc_matrix( - ( - fully_reduced_jacobian_data, - fully_reduced_jacobian_indices, - fully_reduced_jacobian_indptr, - ), - shape=fully_reduced_jacobian_shape, - ) - - # Cache the indices and indptr - self.fully_reduced_jacobian_indices = fully_reduced_jacobian_indices.copy() - self.fully_reduced_jacobian_indptr = fully_reduced_jacobian_indptr.copy() - self.fully_reduced_jacobian_shape = fully_reduced_jacobian_shape - - # Step 4: Identify inclusions (index arrays) - - # Define reduced system indices wrt full system - self.reduced_system_indices = np.concatenate( - [self.potential_indices, self.lagrange_multiplier_indices] - ) - - # Define fully reduced system indices wrt reduced system - need to remove cell - # (and implicitly lagrange multiplier) - self.fully_reduced_system_indices = np.delete( - np.arange(self.grid.num_cells), self.constrained_cell_flat_index - ) - - # Define fully reduced system indices wrt full system - self.fully_reduced_system_indices_full = self.reduced_system_indices[ - self.fully_reduced_system_indices - ] - - def remove_flux(self, jacobian: sps.csc_matrix, residual: np.ndarray) -> tuple: - """Remove the flux block from the jacobian and residual. - - Args: - jacobian (sps.csc_matrix): jacobian - residual (np.ndarray): residual - - Returns: - tuple: reduced jacobian, reduced residual, inverse of flux block - - """ - # Build Schur complement wrt flux-block - J_inv = sps.diags(1.0 / jacobian.diagonal()[self.flux_slice]) - schur_complement = self.D.dot(J_inv.dot(self.DT)) - - # Gauss eliminiation on matrices - reduced_jacobian = self.jacobian_subblock + schur_complement - - # Gauss elimination on vectors - reduced_residual = residual[self.reduced_system_slice].copy() - reduced_residual -= self.D.dot(J_inv.dot(residual[self.flux_slice])) - - return reduced_jacobian, reduced_residual, J_inv - - def remove_lagrange_multiplier(self, jacobian, residual, solution) -> tuple: - """Shortcut for removing the lagrange multiplier from the reduced jacobian. - - Args: - - solution (np.ndarray): solution, TODO make function independent of solution - - Returns: - tuple: fully reduced jacobian, fully reduced residual - - """ - # Make sure the jacobian is a CSC matrix - assert isinstance(jacobian, sps.csc_matrix), "Jacobian should be a CSC matrix." - - # Effective Gauss-elimination for the particular case of the lagrange multiplier - self.fully_reduced_jacobian.data[:] = np.delete( - self.reduced_jacobian.data.copy(), self.rm_indices - ) - # NOTE: The indices have to be restored if the LU factorization is to be used - # FIXME omit if not required - self.fully_reduced_jacobian.indices = self.fully_reduced_jacobian_indices.copy() - - # Rhs is not affected by Gauss elimination as it is assumed that the residual - # is zero in the constrained cell, and the pressure is zero there as well. - # If not, we need to do a proper Gauss elimination on the right hand side! - if abs(residual[-1]) > 1e-6: - raise NotImplementedError("Implementation requires residual to be zero.") - if abs(solution[self.grid.num_faces + self.constrained_cell_flat_index]) > 1e-6: - raise NotImplementedError("Implementation requires solution to be zero.") - fully_reduced_residual = self.reduced_residual[ - self.fully_reduced_system_indices - ].copy() - - return self.fully_reduced_jacobian, fully_reduced_residual - def _shrink( self, flat_flux: np.ndarray, @@ -1688,7 +1482,7 @@ def _solve(self, flat_mass_diff): # 3. Solve linear system with trust in current flux. tic = time.time() rhs_i = rhs.copy() - rhs_i[: self.grid.num_faces] = self.L * self.mass_matrix_faces.dot( + rhs_i[self.flux_slice] = self.L * self.mass_matrix_faces.dot( new_aux_flux - new_force ) new_flux, _, _ = self.split_solution( @@ -1741,7 +1535,7 @@ def _solve(self, flat_mass_diff): # 1. Make relaxation step (solve quadratic optimization problem) tic = time.time() rhs_i = rhs.copy() - rhs_i[: self.grid.num_faces] = weight * self.mass_matrix_faces.dot( + rhs_i[self.flux_slice] = weight * self.mass_matrix_faces.dot( old_aux_flux - old_force ) solution_i = np.zeros_like(rhs_i, dtype=float) @@ -2015,7 +1809,7 @@ def _solve(self, flat_mass_diff): # TODO solve for potential and multiplier solution_i = np.zeros_like(rhs) - solution_i[: self.grid.num_faces] = new_flux.copy() + solution_i[self.flux_slice] = new_flux.copy() # TODO continue # Define performance metric From 548ec888e784ba814f71e6f62724c416716123ae Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Mon, 18 Sep 2023 09:22:52 +0200 Subject: [PATCH 060/100] MAINT: Add fv utils to darsia. --- src/darsia/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/darsia/__init__.py b/src/darsia/__init__.py index 1bb8e4d2..74896c5b 100644 --- a/src/darsia/__init__.py +++ b/src/darsia/__init__.py @@ -28,7 +28,7 @@ from darsia.utils.andersonacceleration import * from darsia.utils.dtype import * from darsia.utils.grid import * -# from darsia.utils.fv import * +from darsia.utils.fv import * from darsia.corrections.basecorrection import * from darsia.corrections.shape.curvature import * from darsia.corrections.shape.affine import * From e81e2905d7f61cb20124aa94413b463815cd90e8 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Mon, 18 Sep 2023 09:32:43 +0200 Subject: [PATCH 061/100] MAINT: strip down code --- src/darsia/measure/wasserstein.py | 60 +++++++++++-------------------- 1 file changed, 20 insertions(+), 40 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index 0bc97c2c..12c29d7c 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -63,9 +63,8 @@ def __init__( # Cache geometrical infos self.grid = grid self.voxel_size = grid.voxel_size - self.dim = grid.dim - assert self.dim == 2, "Currently only 2D images are supported." + assert self.grid.dim == 2, "Currently only 2D images are supported." self.options = options self.regularization = self.options.get("regularization", 0.0) @@ -82,10 +81,9 @@ def _setup(self) -> None: # The following degrees of freedom are considered (also in this order): # - flat fluxes (normal fluxes on the faces) # - flat potentials (potentials on the cells) - # - lagrange multiplier (scalar variable) - - # Idea: Fix the potential in the center of the domain to zero. This is done by - # adding a constraint to the potential via a Lagrange multiplier. + # - lagrange multiplier (scalar variable) - Idea: Fix the potential in the + # center of the domain to zero. This is done by adding a constraint to the + # potential via a Lagrange multiplier. num_flux_dofs = self.grid.num_faces num_potential_dofs = self.grid.num_cells @@ -186,25 +184,6 @@ def _setup(self) -> None: ) """darsia.AndersonAcceleration: Anderson acceleration""" - def split_solution( - self, solution: np.ndarray - ) -> tuple[np.ndarray, np.ndarray, float]: - """Split the solution into (flat) fluxes, potential and lagrange multiplier. - - Args: - solution (np.ndarray): solution - - Returns: - tuple: fluxes, potential, lagrange multiplier - - """ - # Split the solution - flat_flux = solution[self.flux_slice] - flat_potential = solution[self.potential_slice] - flat_lagrange_multiplier = solution[self.lagrange_multiplier_slice] - - return flat_flux, flat_potential, flat_lagrange_multiplier - # ! ---- Projections inbetween faces and cells ---- def face_to_cell(self, flat_flux: np.ndarray) -> np.ndarray: @@ -234,7 +213,7 @@ def face_to_cell(self, flat_flux: np.ndarray) -> np.ndarray: # Determine a cell-based Raviart-Thomas reconstruction of the fluxes, projected # onto piecewise constant functions. - cell_flux = np.zeros((*self.grid.shape, self.dim), dtype=float) + cell_flux = np.zeros((*self.grid.shape, self.grid.dim), dtype=float) # Horizontal fluxes cell_flux[:, :-1, 0] += 0.5 * horizontal_fluxes cell_flux[:, 1:, 0] += 0.5 * horizontal_fluxes @@ -347,7 +326,7 @@ def transport_density(self, cell_flux: np.ndarray) -> np.ndarray: # """ # # Compute transport density - # flat_flux, _, _ = self.split_solution(solution) + # flat_flux = solution[self.flux_slice] # cell_flux = self.face_to_cell(flat_flux) # norm = np.linalg.norm(cell_flux, 2, axis=-1) # return norm @@ -629,7 +608,8 @@ def __call__( distance, solution, status = self._solve(flat_mass_diff) # Split the solution - flat_flux, flat_potential, _ = self.split_solution(solution) + flat_flux = solution[self.flux_slice] + flat_potential = solution[self.potential_slice] # Reshape the fluxes and potential to grid format flux = self.face_to_cell(flat_flux) @@ -784,7 +764,7 @@ def residual(self, rhs: np.ndarray, solution: np.ndarray) -> np.ndarray: np.ndarray: residual """ - flat_flux, _, _ = self.split_solution(solution) + flat_flux = solution[self.flux_slice] mode = self.options.get("mode", "face_arithmetic") flat_flux_norm = np.maximum( self.vector_face_flux_norm(flat_flux, mode=mode), self.regularization @@ -807,7 +787,7 @@ def jacobian(self, solution: np.ndarray) -> sps.linalg.LinearOperator: sps.linalg.splu: LU factorization of the jacobian """ - flat_flux, _, _ = self.split_solution(solution) + flat_flux = solution[self.flux_slice] mode = self.options.get("mode", "face_arithmetic") flat_flux_norm = np.maximum( self.vector_face_flux_norm(flat_flux, mode=mode), self.regularization @@ -1069,7 +1049,7 @@ def _solve(self, flat_mass_diff: np.ndarray) -> tuple[float, np.ndarray, dict]: for iter in range(num_iter): # Keep track of old flux, and old distance old_solution_i = solution_i.copy() - old_flux, _, _ = self.split_solution(solution_i) + old_flux = solution_i[self.flux_slice] old_distance = self.l1_dissipation(old_flux, "cell_arithmetic") # Newton step @@ -1093,7 +1073,7 @@ def _solve(self, flat_mass_diff: np.ndarray) -> tuple[float, np.ndarray, dict]: stats_i.append(time_anderson) # Update distance - new_flux, _, _ = self.split_solution(solution_i) + new_flux = solution_i[self.flux_slice] new_distance = self.l1_dissipation(new_flux, "cell_arithmetic") # Compute the error: @@ -1417,7 +1397,7 @@ def _solve(self, flat_mass_diff): raise NotImplementedError(f"Linear solver {linear_solver} not supported") # Extract intial values - old_flux, _, _ = self.split_solution(solution_i) + old_flux = solution_i[self.flux_slice] old_aux_flux = self._shrink(old_flux, shrink_factor, shrink_mode) old_force = old_flux - old_aux_flux old_distance = self.l1_dissipation(old_flux, dissipation_mode) @@ -1433,9 +1413,9 @@ def _solve(self, flat_mass_diff): rhs_i[self.flux_slice] = self.L * self.mass_matrix_faces.dot( old_aux_flux - old_force ) - new_flux, _, _ = self.split_solution( - self.l_scheme_mixed_darcy_solver.solve(rhs_i) - ) + new_flux = self.l_scheme_mixed_darcy_solver.solve(rhs_i)[ + self.flux_slice + ] time_linearization = time.time() - tic # 2. Shrink step for vectorial fluxes. To comply with the RT0 setting, the @@ -1485,9 +1465,9 @@ def _solve(self, flat_mass_diff): rhs_i[self.flux_slice] = self.L * self.mass_matrix_faces.dot( new_aux_flux - new_force ) - new_flux, _, _ = self.split_solution( - self.l_scheme_mixed_darcy_solver.solve(rhs_i) - ) + new_flux = self.l_scheme_mixed_darcy_solver.solve(rhs_i)[ + self.flux_slice + ] time_linearization = time.time() - tic # Apply Anderson acceleration to flux contribution (the only nonlinear part). @@ -1656,7 +1636,7 @@ def _solve(self, flat_mass_diff): f"""AMG step: {res_amg}""" ) - new_flux, _, _ = self.split_solution(solution_i) + new_flux = solution_i[self.flux_slice] time_linearization = time.time() - tic # 2. Shrink step for vectorial fluxes. To comply with the RT0 setting, the From 739e8f68d32ef49992f95907e6d83f80d50d2c98 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Mon, 18 Sep 2023 09:45:09 +0200 Subject: [PATCH 062/100] MAINT: Reorganize setup --- src/darsia/measure/wasserstein.py | 64 +++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index 12c29d7c..940e4760 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -70,46 +70,59 @@ def __init__( self.regularization = self.options.get("regularization", 0.0) self.verbose = self.options.get("verbose", False) - # Setup of finite volume discretization - self._setup() + # Setup of finite volume discretization and acceleration + self._setup_dof_management() + self._setup_discretization() + self._setup_acceleration() - def _setup(self) -> None: - """Setup of fixed discretization""" + def _setup_dof_management(self) -> None: + """Setup of DOF management. - # ! ---- DOF management ---- - - # The following degrees of freedom are considered (also in this order): - # - flat fluxes (normal fluxes on the faces) - # - flat potentials (potentials on the cells) - # - lagrange multiplier (scalar variable) - Idea: Fix the potential in the - # center of the domain to zero. This is done by adding a constraint to the - # potential via a Lagrange multiplier. + The following degrees of freedom are considered (also in this order): + - flat fluxes (normal fluxes on the faces) + - flat potentials (potentials on the cells) + - lagrange multiplier (scalar variable) - Idea: Fix the potential in the + center of the domain to zero. This is done by adding a constraint to the + potential via a Lagrange multiplier. + """ + # ! ---- Number of dofs ---- num_flux_dofs = self.grid.num_faces num_potential_dofs = self.grid.num_cells num_lagrange_multiplier_dofs = 1 - num_dofs = ( - num_flux_dofs + num_potential_dofs + num_lagrange_multiplier_dofs - ) # total number of dofs + num_dofs = num_flux_dofs + num_potential_dofs + num_lagrange_multiplier_dofs + # ! ---- Indices in global system ---- self.flux_indices = np.arange(num_flux_dofs) + """np.ndarray: indices of the fluxes""" + self.potential_indices = np.arange( num_flux_dofs, num_flux_dofs + num_potential_dofs ) + """np.ndarray: indices of the potentials""" + self.lagrange_multiplier_indices = np.array( [num_flux_dofs + num_potential_dofs], dtype=int ) + """np.ndarray: indices of the lagrange multiplier""" + # ! ---- Fast access to components through slices ---- self.flux_slice = slice(0, num_flux_dofs) + """slice: slice for the fluxes""" + self.potential_slice = slice(num_flux_dofs, num_flux_dofs + num_potential_dofs) + """slice: slice for the potentials""" + self.lagrange_multiplier_slice = slice( num_flux_dofs + num_potential_dofs, num_flux_dofs + num_potential_dofs + num_lagrange_multiplier_dofs, ) - self.reduced_system_slice = slice(num_flux_dofs, None) + """slice: slice for the lagrange multiplier""" - # ! --- Embedding operators --- + self.reduced_system_slice = slice(num_flux_dofs, None) + """slice: slice for the reduced system (potentials and lagrange multiplier)""" + # Embedding operators self.flux_embedding = sps.csc_matrix( ( np.ones(num_flux_dofs, dtype=float), @@ -119,12 +132,16 @@ def _setup(self) -> None: ) """sps.csc_matrix: embedding operator for fluxes""" + def _setup_discretization(self) -> None: + """Setup of fixed discretization operators.""" + # ! ---- Constraint for the potential correpsonding to Lagrange multiplier ---- center_cell = np.array(self.grid.shape) // 2 self.constrained_cell_flat_index = np.ravel_multi_index( center_cell, self.grid.shape ) + num_potential_dofs = self.grid.num_cells self.potential_constraint = sps.csc_matrix( ( np.ones(1, dtype=float), @@ -172,6 +189,9 @@ def _setup(self) -> None: ) """sps.csc_matrix: initial Darcy operator""" + def _setup_acceleration(self) -> None: + """Setup of acceleration methods.""" + # ! ---- Acceleration ---- aa_depth = self.options.get("aa_depth", 0) aa_restart = self.options.get("aa_restart", None) @@ -1152,9 +1172,13 @@ def _solve(self, flat_mass_diff: np.ndarray) -> tuple[float, np.ndarray, dict]: class WassersteinDistanceBregman(VariationalWassersteinDistance): - def _setup(self) -> None: - """Setup the problem.""" - super()._setup() + # TODO __init__ correct method? + def __init__( + self, + grid: darsia.Grid, + options: dict = {}, + ) -> None: + super().__init__(grid, options) self.L = self.options.get("L", 1.0) """Penality parameter for the Bregman iteration.""" From 4fb928e016650c0cf439a2590ad8499397f19115 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Mon, 18 Sep 2023 10:12:33 +0200 Subject: [PATCH 063/100] MAINT: Move FV type projections to fv file. --- src/darsia/measure/wasserstein.py | 166 ++++-------------------------- src/darsia/utils/fv.py | 126 +++++++++++++++++++++++ 2 files changed, 147 insertions(+), 145 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index 940e4760..e8ea0676 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -204,152 +204,24 @@ def _setup_acceleration(self) -> None: ) """darsia.AndersonAcceleration: Anderson acceleration""" - # ! ---- Projections inbetween faces and cells ---- - - def face_to_cell(self, flat_flux: np.ndarray) -> np.ndarray: - """Reconstruct the fluxes on the cells from the fluxes on the faces. - - Use the Raviart-Thomas reconstruction of the fluxes on the cells from the fluxes - on the faces, and use arithmetic averaging of the fluxes on the faces, - equivalent with the L2 projection of the fluxes on the faces to the fluxes on - the cells. - - Matrix-free implementation. - - Args: - flat_flux (np.ndarray): flat fluxes (normal fluxes on the faces) - - Returns: - np.ndarray: cell-based vectorial fluxes - - """ - # Reshape fluxes - use duality of faces and normals - horizontal_fluxes = flat_flux[: self.grid.num_faces_axis[0]].reshape( - self.grid.vertical_faces_shape - ) - vertical_fluxes = flat_flux[self.grid.num_faces_axis[0] :].reshape( - self.grid.horizontal_faces_shape - ) - - # Determine a cell-based Raviart-Thomas reconstruction of the fluxes, projected - # onto piecewise constant functions. - cell_flux = np.zeros((*self.grid.shape, self.grid.dim), dtype=float) - # Horizontal fluxes - cell_flux[:, :-1, 0] += 0.5 * horizontal_fluxes - cell_flux[:, 1:, 0] += 0.5 * horizontal_fluxes - # Vertical fluxes - cell_flux[:-1, :, 1] += 0.5 * vertical_fluxes - cell_flux[1:, :, 1] += 0.5 * vertical_fluxes - - return cell_flux - - def cell_to_face(self, cell_qty: np.ndarray, mode: str) -> np.ndarray: - """Project scalar cell quantity to scalr face quantity. - - Allow for arithmetic or harmonic averaging of the cell quantity to the faces. In - the harmonic case, the averaging is regularized to avoid division by zero. - Matrix-free implementation. - - Args: - cell_qty (np.ndarray): scalar-valued cell-based quantity - mode (str): mode of projection, either "arithmetic" or "harmonic" - (averaging) - - Returns: - np.ndarray: face-based quantity - - """ - # Determine the fluxes on the faces - if mode == "arithmetic": - # Employ arithmetic averaging - horizontal_face_qty = 0.5 * (cell_qty[:, :-1] + cell_qty[:, 1:]) - vertical_face_qty = 0.5 * (cell_qty[:-1, :] + cell_qty[1:, :]) - elif mode == "harmonic": - # Employ harmonic averaging - arithmetic_avg_horizontal = 0.5 * (cell_qty[:, :-1] + cell_qty[:, 1:]) - arithmetic_avg_vertical = 0.5 * (cell_qty[:-1, :] + cell_qty[1:, :]) - # Regularize to avoid division by zero - regularization = 1e-10 - arithmetic_avg_horizontal = ( - arithmetic_avg_horizontal - + (2 * np.sign(arithmetic_avg_horizontal) + 1) * regularization - ) - arithmetic_avg_vertical = ( - 0.5 * arithmetic_avg_vertical - + (2 * np.sign(arithmetic_avg_vertical) + 1) * regularization - ) - product_horizontal = np.multiply(cell_qty[:, :-1], cell_qty[:, 1:]) - product_vertical = np.multiply(cell_qty[:-1, :], cell_qty[1:, :]) - - # Determine the harmonic average - horizontal_face_qty = product_horizontal / arithmetic_avg_horizontal - vertical_face_qty = product_vertical / arithmetic_avg_vertical - else: - raise ValueError(f"Mode {mode} not supported.") - - # Reshape the fluxes - hardcoding the connectivity here - face_qty = np.concatenate( - [horizontal_face_qty.ravel(), vertical_face_qty.ravel()] - ) - - return face_qty - - # NOTE: Currently not in use. TODO rm? - # def face_restriction(self, cell_flux: np.ndarray) -> np.ndarray: - # """Restrict vector-valued fluxes on cells to normal components on faces. - # - # Matrix-free implementation. The fluxes on the faces are determined by - # arithmetic averaging of the fluxes on the cells in the direction of the normal - # of the face. - # - # Args: - # cell_flux (np.ndarray): cell-based fluxes - # - # Returns: - # np.ndarray: face-based fluxes - # - # """ - # # Determine the fluxes on the faces through arithmetic averaging - # horizontal_fluxes = 0.5 * (cell_flux[:, :-1, 0] + cell_flux[:, 1:, 0]) - # vertical_fluxes = 0.5 * (cell_flux[:-1, :, 1] + cell_flux[1:, :, 1]) - # - # # Reshape the fluxes - # flat_flux = np.concatenate( - # [horizontal_fluxes.ravel(), vertical_fluxes.ravel()], axis=0 - # ) - # - # return flat_flux - # ! ---- Effective quantities ---- - def transport_density(self, cell_flux: np.ndarray) -> np.ndarray: - """Compute the transport density of the solution. + def compute_transport_density(self, solution: np.ndarray) -> np.ndarray: + """Compute the transport density from the solution. Args: - flat_flux (np.ndarray): flat fluxes + solution (np.ndarray): solution Returns: np.ndarray: transport density - """ - return np.linalg.norm(cell_flux, 2, axis=-1) - - # TODO consider to replace transport_density with this function: - - # def compute_transport_density(self, solution: np.ndarray) -> np.ndarray: - # """Compute the transport density from the solution. - - # Args: - # solution (np.ndarray): solution - # Returns: - # np.ndarray: transport density - - # """ - # # Compute transport density - # flat_flux = solution[self.flux_slice] - # cell_flux = self.face_to_cell(flat_flux) - # norm = np.linalg.norm(cell_flux, 2, axis=-1) - # return norm + """ + # Convert (scalar) normal fluxes to vector-valued fluxes on cells + flat_flux = solution[self.flux_slice] + cell_flux = darsia.face_to_cell(self.grid, flat_flux) + # Simply take the norm without any other integration + norm = np.linalg.norm(cell_flux, 2, axis=-1) + return norm def l1_dissipation(self, flat_flux: np.ndarray, mode: str) -> float: """Compute the l1 dissipation potential of the solution. @@ -362,7 +234,7 @@ def l1_dissipation(self, flat_flux: np.ndarray, mode: str) -> float: """ if mode == "cell_arithmetic": - cell_flux = self.face_to_cell(flat_flux) + cell_flux = darsia.face_to_cell(self.grid, flat_flux) cell_flux_norm = np.ravel(np.linalg.norm(cell_flux, 2, axis=-1)) return self.mass_matrix_cells.dot(cell_flux_norm).sum() elif mode == "face_arithmetic": @@ -389,14 +261,16 @@ def vector_face_flux_norm(self, flat_flux: np.ndarray, mode: str) -> np.ndarray: # Determine the norm of the fluxes on the faces if mode in ["cell_arithmetic", "cell_harmonic"]: # Consider the piecewise constant projection of vector valued fluxes - cell_flux = self.face_to_cell(flat_flux) + cell_flux = darsia.face_to_cell(self.grid, flat_flux) # Determine the norm of the fluxes on the cells cell_flux_norm = np.maximum( np.linalg.norm(cell_flux, 2, axis=-1), self.regularization ) # Determine averaging mode from mode - either arithmetic or harmonic average_mode = mode.split("_")[1] - flat_flux_norm = self.cell_to_face(cell_flux_norm, mode=average_mode) + flat_flux_norm = darsia.cell_to_face( + self.grid, cell_flux_norm, mode=average_mode + ) elif mode == "face_arithmetic": # Define natural vector valued flux on faces (taking arithmetic averages @@ -632,11 +506,11 @@ def __call__( flat_potential = solution[self.potential_slice] # Reshape the fluxes and potential to grid format - flux = self.face_to_cell(flat_flux) + flux = darsia.face_to_cell(self.grid, flat_flux) potential = flat_potential.reshape(self.grid.shape) # Determine transport density - transport_density = self.transport_density(flux) + transport_density = self.compute_transport_density(solution) # Stop taking time toc = time.time() @@ -1208,12 +1082,14 @@ def _shrink( # Idea: Determine the shrink factor based on the cell reconstructions of the # fluxes. Convert cell-based shrink factors to face-based shrink factors # through arithmetic averaging. - cell_flux = self.face_to_cell(flat_flux) + cell_flux = darsia.face_to_cell(self.grid, flat_flux) norm = np.linalg.norm(cell_flux, 2, axis=-1) cell_scaling = np.maximum(norm - shrink_factor, 0) / ( norm + self.regularization ) - flat_scaling = self.cell_to_face(cell_scaling, mode="arithmetic") + flat_scaling = darsia.cell_to_face( + self.grid, cell_scaling, mode="arithmetic" + ) elif mode == "face_arithmetic": # Define natural vector valued flux on faces (taking arithmetic averages diff --git a/src/darsia/utils/fv.py b/src/darsia/utils/fv.py index 0e9d0f99..fce504dc 100644 --- a/src/darsia/utils/fv.py +++ b/src/darsia/utils/fv.py @@ -5,6 +5,8 @@ import darsia +# ! ---- Finite volume operators ---- + class FVDivergence: """Finite volume divergence operator.""" @@ -252,3 +254,127 @@ def __init__(self, grid: darsia.Grid) -> None: # Cache self.mat = orthogonal_face_average + + +# ! ---- Finite volume projection operators ---- + + +def face_to_cell(grid: darsia.Grid, flat_flux: np.ndarray) -> np.ndarray: + """Reconstruct the vector fluxes on the cells from normal fluxes on the faces. + + Use the Raviart-Thomas reconstruction of the fluxes on the cells from the fluxes + on the faces, and use arithmetic averaging of the fluxes on the faces, + equivalent with the L2 projection of the fluxes on the faces to the fluxes on + the cells. + + Matrix-free implementation. + + Args: + grid (darsia.Grid): grid + flat_flux (np.ndarray): flat fluxes (normal fluxes on the faces) + + Returns: + np.ndarray: cell-based vectorial fluxes + + """ + # Reshape fluxes - use duality of faces and normals + horizontal_fluxes = flat_flux[: grid.num_faces_axis[0]].reshape( + grid.vertical_faces_shape + ) + vertical_fluxes = flat_flux[grid.num_faces_axis[0] :].reshape( + grid.horizontal_faces_shape + ) + + # Determine a cell-based Raviart-Thomas reconstruction of the fluxes, projected + # onto piecewise constant functions. + cell_flux = np.zeros((*grid.shape, grid.dim), dtype=float) + # Horizontal fluxes + cell_flux[:, :-1, 0] += 0.5 * horizontal_fluxes + cell_flux[:, 1:, 0] += 0.5 * horizontal_fluxes + # Vertical fluxes + cell_flux[:-1, :, 1] += 0.5 * vertical_fluxes + cell_flux[1:, :, 1] += 0.5 * vertical_fluxes + + return cell_flux + + +def cell_to_face(grid: darsia.Grid, cell_qty: np.ndarray, mode: str) -> np.ndarray: + """Project scalar cell quantity to scalar face quantity. + + Allow for arithmetic or harmonic averaging of the cell quantity to the faces. In + the harmonic case, the averaging is regularized to avoid division by zero. + Matrix-free implementation. + + Args: + grid (darsia.Grid): grid + cell_qty (np.ndarray): scalar-valued cell-based quantity + mode (str): mode of projection, either "arithmetic" or "harmonic" + (averaging) + + Returns: + np.ndarray: face-based quantity + + """ + + # NOTE: No impact of Grid here, so far! Everything is implicit. This should/could + # change. In particular when switching to 3d! + + # Determine the fluxes on the faces + if mode == "arithmetic": + # Employ arithmetic averaging + horizontal_face_qty = 0.5 * (cell_qty[:, :-1] + cell_qty[:, 1:]) + vertical_face_qty = 0.5 * (cell_qty[:-1, :] + cell_qty[1:, :]) + elif mode == "harmonic": + # Employ harmonic averaging + arithmetic_avg_horizontal = 0.5 * (cell_qty[:, :-1] + cell_qty[:, 1:]) + arithmetic_avg_vertical = 0.5 * (cell_qty[:-1, :] + cell_qty[1:, :]) + # Regularize to avoid division by zero + regularization = 1e-10 + arithmetic_avg_horizontal = ( + arithmetic_avg_horizontal + + (2 * np.sign(arithmetic_avg_horizontal) + 1) * regularization + ) + arithmetic_avg_vertical = ( + 0.5 * arithmetic_avg_vertical + + (2 * np.sign(arithmetic_avg_vertical) + 1) * regularization + ) + product_horizontal = np.multiply(cell_qty[:, :-1], cell_qty[:, 1:]) + product_vertical = np.multiply(cell_qty[:-1, :], cell_qty[1:, :]) + + # Determine the harmonic average + horizontal_face_qty = product_horizontal / arithmetic_avg_horizontal + vertical_face_qty = product_vertical / arithmetic_avg_vertical + else: + raise ValueError(f"Mode {mode} not supported.") + + # Reshape the fluxes - hardcoding the connectivity here + face_qty = np.concatenate([horizontal_face_qty.ravel(), vertical_face_qty.ravel()]) + + return face_qty + + +# NOTE: Currently not in use. TODO rm? +# def face_restriction(self, cell_flux: np.ndarray) -> np.ndarray: +# """Restrict vector-valued fluxes on cells to normal components on faces. +# +# Matrix-free implementation. The fluxes on the faces are determined by +# arithmetic averaging of the fluxes on the cells in the direction of the normal +# of the face. +# +# Args: +# cell_flux (np.ndarray): cell-based fluxes +# +# Returns: +# np.ndarray: face-based fluxes +# +# """ +# # Determine the fluxes on the faces through arithmetic averaging +# horizontal_fluxes = 0.5 * (cell_flux[:, :-1, 0] + cell_flux[:, 1:, 0]) +# vertical_fluxes = 0.5 * (cell_flux[:-1, :, 1] + cell_flux[1:, :, 1]) +# +# # Reshape the fluxes +# flat_flux = np.concatenate( +# [horizontal_fluxes.ravel(), vertical_fluxes.ravel()], axis=0 +# ) +# +# return flat_flux From 4fd167c18bb2dc11e743c7f9d1eed7739b216bf0 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Mon, 18 Sep 2023 10:29:41 +0200 Subject: [PATCH 064/100] MAINT: init reorganization of solver setup --- src/darsia/measure/wasserstein.py | 206 +++++++++++++----------------- 1 file changed, 88 insertions(+), 118 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index e8ea0676..498f09c5 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -70,9 +70,10 @@ def __init__( self.regularization = self.options.get("regularization", 0.0) self.verbose = self.options.get("verbose", False) - # Setup of finite volume discretization and acceleration + # Setup of method self._setup_dof_management() self._setup_discretization() + self._setup_linear_solver() self._setup_acceleration() def _setup_dof_management(self) -> None: @@ -189,6 +190,43 @@ def _setup_discretization(self) -> None: ) """sps.csc_matrix: initial Darcy operator""" + def _setup_linear_solver(self) -> None: + self.linear_solver = self.options.get("linear_solver", "lu") + assert self.linear_solver in [ + "lu", + "lu-flux-reduced", + "amg-flux-reduced", + "lu-potential", + "amg-potential", + ], f"Linear solver {self.linear_solver} not supported." + + if self.linear_solver in ["amg-flux-reduced", "amg-potential"]: + # TODO add possibility for user control + self.ml_options = { + # B=X.reshape( + # n * n, 1 + # ), # the representation of the near null space (this is a poor choice) + # BH=None, # the representation of the left near null space + "symmetry": "hermitian", # indicate that the matrix is Hermitian + # strength="evolution", # change the strength of connection + "aggregate": "standard", # use a standard aggregation method + "smooth": ( + "jacobi", + {"omega": 4.0 / 3.0, "degree": 2}, + ), # prolongation smoothing + "presmoother": ("block_gauss_seidel", {"sweep": "symmetric"}), + "postsmoother": ("block_gauss_seidel", {"sweep": "symmetric"}), + # improve_candidates=[ + # ("block_gauss_seidel", {"sweep": "symmetric", "iterations": 4}), + # None, + # ], + "max_levels": 4, # maximum number of levels + "max_coarse": 1000, # maximum number on a coarse level + # keep=False, # keep extra operators around in the hierarchy (memory) + } + self.tol_amg = self.options.get("linear_solver_tol", 1e-6) + self.res_history_amg = [] + def _setup_acceleration(self) -> None: """Setup of acceleration methods.""" @@ -734,44 +772,9 @@ def linearization_step( # Setup linear solver tic = time.time() - linear_solver = self.options.get("linear_solver", "lu") - assert linear_solver in [ - "lu", - "lu-flux-reduced", - "amg-flux-reduced", - "lu-potential", - "amg-potential", - ], f"Linear solver {linear_solver} not supported." - - if linear_solver in ["amg-flux-reduced", "amg-potential"]: - # TODO add possibility for user control - ml_options = { - # B=X.reshape( - # n * n, 1 - # ), # the representation of the near null space (this is a poor choice) - # BH=None, # the representation of the left near null space - "symmetry": "hermitian", # indicate that the matrix is Hermitian - # strength="evolution", # change the strength of connection - "aggregate": "standard", # use a standard aggregation method - "smooth": ( - "jacobi", - {"omega": 4.0 / 3.0, "degree": 2}, - ), # prolongation smoothing - "presmoother": ("block_gauss_seidel", {"sweep": "symmetric"}), - "postsmoother": ("block_gauss_seidel", {"sweep": "symmetric"}), - # improve_candidates=[ - # ("block_gauss_seidel", {"sweep": "symmetric", "iterations": 4}), - # None, - # ], - "max_levels": 4, # maximum number of levels - "max_coarse": 1000, # maximum number on a coarse level - # keep=False, # keep extra operators around in the hierarchy (memory) - } - tol_amg = self.options.get("linear_solver_tol", 1e-6) - res_history_amg = [] # Solve linear system for the update - if linear_solver == "lu": + if self.linear_solver == "lu": # Solve full system tic = time.time() jacobian_lu = sps.linalg.splu(approx_jacobian) @@ -779,7 +782,7 @@ def linearization_step( tic = time.time() update = jacobian_lu.solve(residual) time_solve = time.time() - tic - elif linear_solver in ["lu-flux-reduced", "amg-flux-reduced"]: + elif self.linear_solver in ["lu-flux-reduced", "amg-flux-reduced"]: # Solve potential-multiplier problem # Reduce flux block @@ -790,22 +793,22 @@ def linearization_step( jacobian_flux_inv, ) = self.remove_flux(approx_jacobian, residual) - if linear_solver == "lu-flux-reduced": + if self.linear_solver == "lu-flux-reduced": lu = sps.linalg.splu(self.reduced_jacobian) time_setup = time.time() - tic tic = time.time() update[self.reduced_system_slice] = lu.solve(self.reduced_residual) - elif linear_solver == "amg-flux-reduced": + elif self.linear_solver == "amg-flux-reduced": ml = pyamg.smoothed_aggregation_solver( - self.reduced_jacobian, **ml_options + self.reduced_jacobian, **self.ml_options ) time_setup = time.time() - tic tic = time.time() update[self.reduced_system_slice] = ml.solve( self.reduced_residual, - tol=tol_amg, - residuals=res_history_amg, + tol=self.tol_amg, + residuals=self.res_history_amg, ) # Compute flux update @@ -815,7 +818,7 @@ def linearization_step( ) time_solve = time.time() - tic - elif linear_solver in ["lu-potential", "amg-potential"]: + elif self.linear_solver in ["lu-potential", "amg-potential"]: # Solve pure potential problem # Reduce flux block @@ -834,7 +837,7 @@ def linearization_step( self.reduced_jacobian, self.reduced_residual, solution ) - if linear_solver == "lu-potential": + if self.linear_solver == "lu-potential": lu = sps.linalg.splu(self.fully_reduced_jacobian) time_setup = time.time() - tic tic = time.time() @@ -842,16 +845,16 @@ def linearization_step( self.fully_reduced_residual ) - elif linear_solver == "amg-potential": + elif self.linear_solver == "amg-potential": ml = pyamg.smoothed_aggregation_solver( - self.fully_reduced_jacobian, **ml_options + self.fully_reduced_jacobian, **self.ml_options ) time_setup = time.time() - tic tic = time.time() update[self.fully_reduced_system_indices_full] = ml.solve( self.fully_reduced_residual, - tol=tol_amg, - residuals=res_history_amg, + tol=self.tol_amg, + residuals=self.res_history_amg, ) # Compute flux update @@ -862,10 +865,10 @@ def linearization_step( time_solve = time.time() - tic # Diagnostics - if linear_solver in ["amg-flux-reduced", "amg-potential"]: + if self.linear_solver in ["amg-flux-reduced", "amg-potential"]: if self.options.get("linear_solver_verbosity", False): - num_amg_iter = len(res_history_amg) - res_amg = res_history_amg[-1] + num_amg_iter = len(self.res_history_amg) + res_amg = self.res_history_amg[-1] print(ml) print( f"#AMG iterations: {num_amg_iter}; Residual after AMG step: {res_amg}" @@ -1123,41 +1126,6 @@ def _solve(self, flat_mass_diff): # Define linear solver to be used to invert the Darcy systems self.setup_infrastructure() - linear_solver = self.options.get("linear_solver", "lu") - assert linear_solver in [ - "lu", - "lu-flux-reduced", - "amg-flux-reduced", - "lu-potential", - "amg-potential", - ], f"Linear solver {linear_solver} not supported." - - if linear_solver in ["amg-flux-reduced", "amg-potential"]: - # TODO add possibility for user control - ml_options = { - # B=X.reshape( - # n * n, 1 - # ), # the representation of the near null space (this is a poor choice) - # BH=None, # the representation of the left near null space - "symmetry": "hermitian", # indicate that the matrix is Hermitian - # strength="evolution", # change the strength of connection - "aggregate": "standard", # use a standard aggregation method - "smooth": ( - "jacobi", - {"omega": 4.0 / 3.0, "degree": 2}, - ), # prolongation smoothing - "presmoother": ("block_gauss_seidel", {"sweep": "symmetric"}), - "postsmoother": ("block_gauss_seidel", {"sweep": "symmetric"}), - # improve_candidates=[ - # ("block_gauss_seidel", {"sweep": "symmetric", "iterations": 4}), - # None, - # ], - "max_levels": 4, # maximum number of levels - "max_coarse": 1000, # maximum number on a coarse level - # keep=False, # keep extra operators around in the hierarchy (memory) - } - tol_amg = self.options.get("linear_solver_tol", 1e-6) - res_history_amg = [] # Relaxation parameter self.L = self.options.get("L", 1.0) @@ -1212,11 +1180,11 @@ def _solve(self, flat_mass_diff): ], format="csc", ) - if linear_solver == "lu": + if self.linear_solver == "lu": self.l_scheme_mixed_darcy_solver = sps.linalg.splu(l_scheme_mixed_darcy) solution_i = self.l_scheme_mixed_darcy_solver.solve(rhs) - elif linear_solver in ["lu-flux-reduced", "amg-flux-reduced"]: + elif self.linear_solver in ["lu-flux-reduced", "amg-flux-reduced"]: # Solve potential-multiplier problem # Reduce flux block @@ -1226,21 +1194,21 @@ def _solve(self, flat_mass_diff): jacobian_flux_inv, ) = self.remove_flux(l_scheme_mixed_darcy, rhs) - if linear_solver == "lu-flux-reduced": + if self.linear_solver == "lu-flux-reduced": self.l_scheme_mixed_darcy_solver = sps.linalg.splu( self.reduced_jacobian ) - elif linear_solver == "amg-flux-reduced": + elif self.linear_solver == "amg-flux-reduced": self.l_scheme_mixed_darcy_solver = pyamg.smoothed_aggregation_solver( - self.reduced_jacobian, **ml_options + self.reduced_jacobian, **self.ml_options ) solution_i[ self.reduced_system_slice ] = self.l_scheme_mixed_darcy_solver.solve( self.reduced_residual, - tol=tol_amg, - residuals=res_history_amg, + tol=self.tol_amg, + residuals=self.res_history_amg, ) # Compute flux update @@ -1249,7 +1217,7 @@ def _solve(self, flat_mass_diff): + self.DT.dot(solution_i[self.reduced_system_slice]) ) - elif linear_solver in ["lu-potential", "amg-potential"]: + elif self.linear_solver in ["lu-potential", "amg-potential"]: # Solve pure potential problem # Reduce flux block @@ -1267,7 +1235,7 @@ def _solve(self, flat_mass_diff): self.reduced_jacobian, self.reduced_residual, solution_i ) - if linear_solver == "lu-potential": + if self.linear_solver == "lu-potential": self.l_scheme_mixed_darcy_solver = sps.linalg.splu( self.fully_reduced_jacobian ) @@ -1275,16 +1243,16 @@ def _solve(self, flat_mass_diff): self.fully_reduced_system_indices_full ] = self.l_scheme_mixed_darcy_solver.solve(self.fully_reduced_residual) - elif linear_solver == "amg-potential": + elif self.linear_solver == "amg-potential": self.l_scheme_mixed_darcy_solver = pyamg.smoothed_aggregation_solver( - self.fully_reduced_jacobian, **ml_options + self.fully_reduced_jacobian, **self.ml_options ) solution_i[ self.fully_reduced_system_indices_full ] = self.l_scheme_mixed_darcy_solver.solve( self.fully_reduced_residual, - tol=tol_amg, - residuals=res_history_amg, + tol=self.tol_amg, + residuals=self.res_history_amg, ) # Compute flux update @@ -1294,7 +1262,9 @@ def _solve(self, flat_mass_diff): ) else: - raise NotImplementedError(f"Linear solver {linear_solver} not supported") + raise NotImplementedError( + f"Linear solver {self.linear_solver} not supported" + ) # Extract intial values old_flux = solution_i[self.flux_slice] @@ -1420,14 +1390,14 @@ def _solve(self, flat_mass_diff): ) solution_i = np.zeros_like(rhs_i, dtype=float) - if linear_solver == "lu": + if self.linear_solver == "lu": if update_solver: self.l_scheme_mixed_darcy_solver = sps.linalg.splu( l_scheme_mixed_darcy ) solution_i = self.l_scheme_mixed_darcy_solver.solve(rhs_i) - elif linear_solver in ["lu-flux-reduced", "amg-flux-reduced"]: + elif self.linear_solver in ["lu-flux-reduced", "amg-flux-reduced"]: # Solve potential-multiplier problem # Reduce flux block @@ -1437,7 +1407,7 @@ def _solve(self, flat_mass_diff): jacobian_flux_inv, ) = self.remove_flux(l_scheme_mixed_darcy, rhs_i) - if linear_solver == "lu-flux-reduced": + if self.linear_solver == "lu-flux-reduced": if update_solver: self.l_scheme_mixed_darcy_solver = sps.linalg.splu( self.reduced_jacobian @@ -1448,19 +1418,19 @@ def _solve(self, flat_mass_diff): self.reduced_residual ) - elif linear_solver == "amg-flux-reduced": + elif self.linear_solver == "amg-flux-reduced": if update_solver: self.l_scheme_mixed_darcy_solver = ( pyamg.smoothed_aggregation_solver( - self.reduced_jacobian, **ml_options + self.reduced_jacobian, **self.ml_options ) ) solution_i[ self.reduced_system_slice ] = self.l_scheme_mixed_darcy_solver.solve( self.reduced_residual, - tol=tol_amg, - residuals=res_history_amg, + tol=self.tol_amg, + residuals=self.res_history_amg, ) # Compute flux update @@ -1469,7 +1439,7 @@ def _solve(self, flat_mass_diff): + self.DT.dot(solution_i[self.reduced_system_slice]) ) - elif linear_solver in ["lu-potential", "amg-potential"]: + elif self.linear_solver in ["lu-potential", "amg-potential"]: # Solve pure potential problem # Reduce flux block @@ -1487,7 +1457,7 @@ def _solve(self, flat_mass_diff): self.reduced_jacobian, self.reduced_residual, solution_i ) - if linear_solver == "lu-potential": + if self.linear_solver == "lu-potential": if update_solver: self.l_scheme_mixed_darcy_solver = sps.linalg.splu( self.fully_reduced_jacobian @@ -1498,11 +1468,11 @@ def _solve(self, flat_mass_diff): self.fully_reduced_residual ) - elif linear_solver == "amg-potential": + elif self.linear_solver == "amg-potential": if update_solver: self.l_scheme_mixed_darcy_solver = ( pyamg.smoothed_aggregation_solver( - self.fully_reduced_jacobian, **ml_options + self.fully_reduced_jacobian, **self.ml_options ) ) # time_setup = time.time() - tic @@ -1511,8 +1481,8 @@ def _solve(self, flat_mass_diff): self.fully_reduced_system_indices_full ] = self.l_scheme_mixed_darcy_solver.solve( self.fully_reduced_residual, - tol=tol_amg, - residuals=res_history_amg, + tol=self.tol_amg, + residuals=self.res_history_amg, ) # Compute flux update @@ -1522,14 +1492,14 @@ def _solve(self, flat_mass_diff): ) else: raise NotImplementedError( - f"Linear solver {linear_solver} not supported" + f"""Linear solver {self.linear_solver} not supported.""" ) # Diagnostics - if linear_solver in ["amg-flux-reduced", "amg-potential"]: + if self.linear_solver in ["amg-flux-reduced", "amg-potential"]: if self.options.get("linear_solver_verbosity", False): - num_amg_iter = len(res_history_amg) - res_amg = res_history_amg[-1] + num_amg_iter = len(self.res_history_amg) + res_amg = self.res_history_amg[-1] print(self.l_scheme_mixed_darcy_solver) print( f"""#AMG iterations: {num_amg_iter}; Residual after """ From 3ded5853b686f06df048522e9ff71e6c885b745f Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Mon, 18 Sep 2023 10:36:37 +0200 Subject: [PATCH 065/100] DOC/MAINT: Reorganize definition of L --- src/darsia/measure/wasserstein.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index 498f09c5..b67ead5c 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -683,7 +683,19 @@ def _plot_solution( class WassersteinDistanceNewton(VariationalWassersteinDistance): - """Class to determine the L1 EMD/Wasserstein distance solved with Newton's method.""" + """Class to determine the L1 EMD/Wasserstein distance solved with Newton's method. + + Here, self.L has the interpretation of a lower cut-off value in the linearization + only. With such relaxation, the BEckman problem itself is not regularized, but + instead the solution trajectory is merely affected. + + """ + + def __init__(self, grid, options) -> None: + super().__init__(grid, options) + + self.L = self.options.get("L", 1.0) + """float: relaxation parameter, lower cut-off for the mobility""" def residual(self, rhs: np.ndarray, solution: np.ndarray) -> np.ndarray: """Compute the residual of the solution. @@ -893,9 +905,6 @@ def _solve(self, flat_mass_diff: np.ndarray) -> tuple[float, np.ndarray, dict]: # and therefore better solutions to mu and u. Higher depth is better, but more # expensive. - self.L = self.options.get("L", 1.0) - """float: relaxation parameter, lower cut-off for the mobility""" - # Setup tic = time.time() self.setup_infrastructure() From 31f9490087f604043ad4b3710268d2ab0c7a8c1c Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Mon, 18 Sep 2023 12:27:04 +0200 Subject: [PATCH 066/100] MAINT: Reorganize linear solution - for Newton --- src/darsia/measure/wasserstein.py | 540 ++++++++++++++++-------------- 1 file changed, 281 insertions(+), 259 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index b67ead5c..cd6c3c48 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -5,7 +5,7 @@ import time from pathlib import Path -from typing import Union +from typing import Optional, Union import matplotlib.pyplot as plt import numpy as np @@ -227,104 +227,13 @@ def _setup_linear_solver(self) -> None: self.tol_amg = self.options.get("linear_solver_tol", 1e-6) self.res_history_amg = [] - def _setup_acceleration(self) -> None: - """Setup of acceleration methods.""" - - # ! ---- Acceleration ---- - aa_depth = self.options.get("aa_depth", 0) - aa_restart = self.options.get("aa_restart", None) - self.anderson = ( - darsia.AndersonAcceleration( - dimension=None, depth=aa_depth, restart=aa_restart - ) - if aa_depth > 0 - else None - ) - """darsia.AndersonAcceleration: Anderson acceleration""" - - # ! ---- Effective quantities ---- - - def compute_transport_density(self, solution: np.ndarray) -> np.ndarray: - """Compute the transport density from the solution. - - Args: - solution (np.ndarray): solution - - Returns: - np.ndarray: transport density - - """ - # Convert (scalar) normal fluxes to vector-valued fluxes on cells - flat_flux = solution[self.flux_slice] - cell_flux = darsia.face_to_cell(self.grid, flat_flux) - # Simply take the norm without any other integration - norm = np.linalg.norm(cell_flux, 2, axis=-1) - return norm - - def l1_dissipation(self, flat_flux: np.ndarray, mode: str) -> float: - """Compute the l1 dissipation potential of the solution. - - Args: - flat_flux (np.ndarray): flat fluxes - - Returns: - float: l1 dissipation potential - - """ - if mode == "cell_arithmetic": - cell_flux = darsia.face_to_cell(self.grid, flat_flux) - cell_flux_norm = np.ravel(np.linalg.norm(cell_flux, 2, axis=-1)) - return self.mass_matrix_cells.dot(cell_flux_norm).sum() - elif mode == "face_arithmetic": - face_flux_norm = self.vector_face_flux_norm(flat_flux, "face_arithmetic") - return self.mass_matrix_faces.dot(face_flux_norm).sum() - - # ! ---- Lumping of effective mobility - - def vector_face_flux_norm(self, flat_flux: np.ndarray, mode: str) -> np.ndarray: - """Compute the norm of the vector-valued fluxes on the faces. - - Args: - flat_flux (np.ndarray): flat fluxes (normal fluxes on the faces) - mode (str): mode of the norm, either "cell_arithmetic", "cell_harmonic" or - "face_arithmetic". In the cell-based modes, the fluxes are projected to - the cells and the norm is computed there. In the face-based mode, the - norm is computed directly on the faces. - - Returns: - np.ndarray: norm of the vector-valued fluxes on the faces - - """ - - # Determine the norm of the fluxes on the faces - if mode in ["cell_arithmetic", "cell_harmonic"]: - # Consider the piecewise constant projection of vector valued fluxes - cell_flux = darsia.face_to_cell(self.grid, flat_flux) - # Determine the norm of the fluxes on the cells - cell_flux_norm = np.maximum( - np.linalg.norm(cell_flux, 2, axis=-1), self.regularization - ) - # Determine averaging mode from mode - either arithmetic or harmonic - average_mode = mode.split("_")[1] - flat_flux_norm = darsia.cell_to_face( - self.grid, cell_flux_norm, mode=average_mode - ) - - elif mode == "face_arithmetic": - # Define natural vector valued flux on faces (taking arithmetic averages - # of continuous fluxes over cells evaluated at faces) - tangential_flux = self.orthogonal_face_average.dot(flat_flux) - # Determine the l2 norm of the fluxes on the faces, add some regularization - flat_flux_norm = np.sqrt(flat_flux**2 + tangential_flux**2) - - else: - raise ValueError(f"Mode {mode} not supported.") - - return flat_flux_norm - - # ! ---- Solver methods ---- + # Setup inrastructure for Schur complement reduction + if self.linear_solver in ["lu-flux-reduced", "amg-flux-reduced"]: + self.setup_one_level_schur_reduction() + elif self.linear_solver in ["lu-potential", "amg-potential"]: + self.setup_two_level_schur_reduction() - def setup_infrastructure(self) -> None: + def setup_one_level_schur_reduction(self) -> None: """Setup the infrastructure for reduced systems through Gauss elimination. Provide internal data structures for the reduced system. @@ -355,7 +264,12 @@ def setup_infrastructure(self) -> None: # Cache the reduced jacobian self.reduced_jacobian = self.jacobian_subblock + schur_complement - # Step 3: Remove potential block through Gauss elimination + def setup_two_level_schur_reduction(self) -> None: + """Additional setup of infrastructure for fully reduced systems.""" + # Step 1 and 2: + self.setup_one_level_schur_reduction() + + # Step 3: Remove Lagrange multiplier block through Gauss elimination # Find row entries to be removed rm_row_entries = np.arange( @@ -442,6 +356,232 @@ def setup_infrastructure(self) -> None: self.fully_reduced_system_indices ] + def _setup_acceleration(self) -> None: + """Setup of acceleration methods.""" + + # ! ---- Acceleration ---- + aa_depth = self.options.get("aa_depth", 0) + aa_restart = self.options.get("aa_restart", None) + self.anderson = ( + darsia.AndersonAcceleration( + dimension=None, depth=aa_depth, restart=aa_restart + ) + if aa_depth > 0 + else None + ) + """darsia.AndersonAcceleration: Anderson acceleration""" + + # ! ---- Effective quantities ---- + + def compute_transport_density(self, solution: np.ndarray) -> np.ndarray: + """Compute the transport density from the solution. + + Args: + solution (np.ndarray): solution + + Returns: + np.ndarray: transport density + + """ + # Convert (scalar) normal fluxes to vector-valued fluxes on cells + flat_flux = solution[self.flux_slice] + cell_flux = darsia.face_to_cell(self.grid, flat_flux) + # Simply take the norm without any other integration + norm = np.linalg.norm(cell_flux, 2, axis=-1) + return norm + + def l1_dissipation(self, flat_flux: np.ndarray, mode: str) -> float: + """Compute the l1 dissipation potential of the solution. + + Args: + flat_flux (np.ndarray): flat fluxes + + Returns: + float: l1 dissipation potential + + """ + if mode == "cell_arithmetic": + cell_flux = darsia.face_to_cell(self.grid, flat_flux) + cell_flux_norm = np.ravel(np.linalg.norm(cell_flux, 2, axis=-1)) + return self.mass_matrix_cells.dot(cell_flux_norm).sum() + elif mode == "face_arithmetic": + face_flux_norm = self.vector_face_flux_norm(flat_flux, "face_arithmetic") + return self.mass_matrix_faces.dot(face_flux_norm).sum() + + # ! ---- Lumping of effective mobility + + def vector_face_flux_norm(self, flat_flux: np.ndarray, mode: str) -> np.ndarray: + """Compute the norm of the vector-valued fluxes on the faces. + + Args: + flat_flux (np.ndarray): flat fluxes (normal fluxes on the faces) + mode (str): mode of the norm, either "cell_arithmetic", "cell_harmonic" or + "face_arithmetic". In the cell-based modes, the fluxes are projected to + the cells and the norm is computed there. In the face-based mode, the + norm is computed directly on the faces. + + Returns: + np.ndarray: norm of the vector-valued fluxes on the faces + + """ + + # Determine the norm of the fluxes on the faces + if mode in ["cell_arithmetic", "cell_harmonic"]: + # Consider the piecewise constant projection of vector valued fluxes + cell_flux = darsia.face_to_cell(self.grid, flat_flux) + # Determine the norm of the fluxes on the cells + cell_flux_norm = np.maximum( + np.linalg.norm(cell_flux, 2, axis=-1), self.regularization + ) + # Determine averaging mode from mode - either arithmetic or harmonic + average_mode = mode.split("_")[1] + flat_flux_norm = darsia.cell_to_face( + self.grid, cell_flux_norm, mode=average_mode + ) + + elif mode == "face_arithmetic": + # Define natural vector valued flux on faces (taking arithmetic averages + # of continuous fluxes over cells evaluated at faces) + tangential_flux = self.orthogonal_face_average.dot(flat_flux) + # Determine the l2 norm of the fluxes on the faces, add some regularization + flat_flux_norm = np.sqrt(flat_flux**2 + tangential_flux**2) + + else: + raise ValueError(f"Mode {mode} not supported.") + + return flat_flux_norm + + # ! ---- Solver methods ---- + + def linear_solve( + self, + matrix: sps.csc_matrix, + rhs: np.ndarray, + previous_solution: Optional[np.ndarray] = None, + ): + if self.linear_solver == "lu": + # Setup LU factorization for the full system + tic = time.time() + lu = sps.linalg.splu(matrix) + time_setup = time.time() - tic + + # Solve the full system + tic = time.time() + solution = lu.solve(rhs) + time_solve = time.time() - tic + + elif self.linear_solver in [ + "lu-flux-reduced", + "amg-flux-reduced", + "lu-potential", + "amg-potential", + ]: + # Solve potential-multiplier problem + + # Allocate memory for solution + solution = np.zeros_like(rhs) + + # Reduce flux block + tic = time.time() + ( + self.reduced_matrix, + self.reduced_rhs, + matrix_flux_inv, + ) = self.remove_flux(matrix, rhs) + + if self.linear_solver == "lu-flux-reduced": + # LU factorization for reduced system + lu = sps.linalg.splu(self.reduced_matrix) + time_setup = time.time() - tic + + # Solve for the potential and lagrange multiplier + tic = time.time() + solution[self.reduced_system_slice] = lu.solve(self.reduced_rhs) + + elif self.linear_solver == "amg-flux-reduced": + # AMG solver for reduced system + ml = pyamg.smoothed_aggregation_solver( + self.reduced_matrix, **self.ml_options + ) + time_setup = time.time() - tic + + # Solve for the potential and lagrange multiplier + tic = time.time() + solution[self.reduced_system_slice] = ml.solve( + self.reduced_rhs, + tol=self.tol_amg, + residuals=self.res_history_amg, + ) + else: + # Solve pure potential problem + + # NOTE: It is implicitly assumed that the lagrange multiplier is zero + # in the constrained cell. This is not checked here. And no update is + # performed. + if ( + abs( + previous_solution[ + self.grid.num_faces + self.constrained_cell_flat_index + ] + ) + > 1e-6 + ): + raise NotImplementedError( + "Implementation requires solution satisfy the constraint." + ) + + # Reduce to pure potential system + ( + self.fully_reduced_matix, + self.fully_reduced_rhs, + ) = self.remove_lagrange_multiplier( + self.reduced_matrix, + self.reduced_rhs, + ) + + if self.linear_solver == "lu-potential": + # Finish LU factorization of the pure potential system + lu = sps.linalg.splu(self.fully_reduced_matrix) + time_setup = time.time() - tic + + # Solve the pure potential system + tic = time.time() + solution[self.fully_reduced_system_indices_full] = lu.solve( + self.fully_reduced_rhs + ) + + elif self.linear_solver == "amg-potential": + # Finish AMG setup of th pure potential system + ml = pyamg.smoothed_aggregation_solver( + self.fully_reduced_jacobian, **self.ml_options + ) + time_setup = time.time() - tic + + # Solve the pure potential system + tic = time.time() + solution[self.fully_reduced_system_indices_full] = ml.solve( + self.fully_reduced_rhs, + tol=self.tol_amg, + residuals=self.res_history_amg, + ) + + # Compute flux update + solution[self.flux_slice] = matrix_flux_inv.dot( + rhs[self.flux_slice] + self.DT.dot(solution[self.reduced_system_slice]) + ) + time_solve = time.time() - tic + + stats = { + "time setup": time_setup, + "time solve": time_solve, + } + if self.linear_solver in ["amg-flux-reduced", "amg-potential"]: + stats["amg residuals"] = self.res_history_amg + stats["amg num iterations"] = len(self.res_history_amg) + stats["amg residual"] = self.res_history_amg[-1] + + return solution, stats + def remove_flux(self, jacobian: sps.csc_matrix, residual: np.ndarray) -> tuple: """Remove the flux block from the jacobian and residual. @@ -466,23 +606,25 @@ def remove_flux(self, jacobian: sps.csc_matrix, residual: np.ndarray) -> tuple: return reduced_jacobian, reduced_residual, J_inv - def remove_lagrange_multiplier(self, jacobian, residual, solution) -> tuple: + def remove_lagrange_multiplier(self, reduced_jacobian, reduced_residual) -> tuple: """Shortcut for removing the lagrange multiplier from the reduced jacobian. Args: - - solution (np.ndarray): solution, TODO make function independent of solution + reduced_jacobian (sps.csc_matrix): reduced jacobian + reduced_residual (np.ndarray): reduced residual Returns: tuple: fully reduced jacobian, fully reduced residual """ # Make sure the jacobian is a CSC matrix - assert isinstance(jacobian, sps.csc_matrix), "Jacobian should be a CSC matrix." + assert isinstance( + reduced_jacobian, sps.csc_matrix + ), "Jacobian should be a CSC matrix." # Effective Gauss-elimination for the particular case of the lagrange multiplier self.fully_reduced_jacobian.data[:] = np.delete( - self.reduced_jacobian.data.copy(), self.rm_indices + reduced_jacobian.data.copy(), self.rm_indices ) # NOTE: The indices have to be restored if the LU factorization is to be used # FIXME omit if not required @@ -491,11 +633,9 @@ def remove_lagrange_multiplier(self, jacobian, residual, solution) -> tuple: # Rhs is not affected by Gauss elimination as it is assumed that the residual # is zero in the constrained cell, and the pressure is zero there as well. # If not, we need to do a proper Gauss elimination on the right hand side! - if abs(residual[-1]) > 1e-6: + if abs(reduced_residual[-1]) > 1e-6: raise NotImplementedError("Implementation requires residual to be zero.") - if abs(solution[self.grid.num_faces + self.constrained_cell_flat_index]) > 1e-6: - raise NotImplementedError("Implementation requires solution to be zero.") - fully_reduced_residual = self.reduced_residual[ + fully_reduced_residual = reduced_residual[ self.fully_reduced_system_indices ].copy() @@ -751,146 +891,6 @@ def jacobian(self, solution: np.ndarray) -> sps.linalg.LinearOperator: ) return approx_jacobian - def linearization_step( - self, solution: np.ndarray, rhs: np.ndarray, iter: int - ) -> tuple[np.ndarray, np.ndarray, list[float]]: - """Newton step for the linearization of the problem. - - In the first iteration, the linearization is the linearization of the Darcy - problem. - - Args: - solution (np.ndarray): solution - rhs (np.ndarray): right hand side - iter (int): iteration number - - Returns: - tuple: update, residual, stats (timinings) - - """ - # Determine residual and (full) Jacobian - tic = time.time() - if iter == 0: - residual = rhs.copy() - approx_jacobian = self.darcy_init.copy() - else: - residual = self.residual(rhs, solution) - approx_jacobian = self.jacobian(solution) - toc = time.time() - time_setup = toc - tic - - # Allocate update - update = np.zeros_like(solution, dtype=float) - - # Setup linear solver - tic = time.time() - - # Solve linear system for the update - if self.linear_solver == "lu": - # Solve full system - tic = time.time() - jacobian_lu = sps.linalg.splu(approx_jacobian) - time_setup = time.time() - tic - tic = time.time() - update = jacobian_lu.solve(residual) - time_solve = time.time() - tic - elif self.linear_solver in ["lu-flux-reduced", "amg-flux-reduced"]: - # Solve potential-multiplier problem - - # Reduce flux block - tic = time.time() - ( - self.reduced_jacobian, - self.reduced_residual, - jacobian_flux_inv, - ) = self.remove_flux(approx_jacobian, residual) - - if self.linear_solver == "lu-flux-reduced": - lu = sps.linalg.splu(self.reduced_jacobian) - time_setup = time.time() - tic - tic = time.time() - update[self.reduced_system_slice] = lu.solve(self.reduced_residual) - - elif self.linear_solver == "amg-flux-reduced": - ml = pyamg.smoothed_aggregation_solver( - self.reduced_jacobian, **self.ml_options - ) - time_setup = time.time() - tic - tic = time.time() - update[self.reduced_system_slice] = ml.solve( - self.reduced_residual, - tol=self.tol_amg, - residuals=self.res_history_amg, - ) - - # Compute flux update - update[self.flux_slice] = jacobian_flux_inv.dot( - residual[self.flux_slice] - + self.DT.dot(update[self.reduced_system_slice]) - ) - time_solve = time.time() - tic - - elif self.linear_solver in ["lu-potential", "amg-potential"]: - # Solve pure potential problem - - # Reduce flux block - tic = time.time() - ( - self.reduced_jacobian, - self.reduced_residual, - jacobian_flux_inv, - ) = self.remove_flux(approx_jacobian, residual) - - # Reduce to pure pressure system - ( - self.fully_reduced_jacobian, - self.fully_reduced_residual, - ) = self.remove_lagrange_multiplier( - self.reduced_jacobian, self.reduced_residual, solution - ) - - if self.linear_solver == "lu-potential": - lu = sps.linalg.splu(self.fully_reduced_jacobian) - time_setup = time.time() - tic - tic = time.time() - update[self.fully_reduced_system_indices_full] = lu.solve( - self.fully_reduced_residual - ) - - elif self.linear_solver == "amg-potential": - ml = pyamg.smoothed_aggregation_solver( - self.fully_reduced_jacobian, **self.ml_options - ) - time_setup = time.time() - tic - tic = time.time() - update[self.fully_reduced_system_indices_full] = ml.solve( - self.fully_reduced_residual, - tol=self.tol_amg, - residuals=self.res_history_amg, - ) - - # Compute flux update - update[self.flux_slice] = jacobian_flux_inv.dot( - residual[self.flux_slice] - + self.DT.dot(update[self.reduced_system_slice]) - ) - time_solve = time.time() - tic - - # Diagnostics - if self.linear_solver in ["amg-flux-reduced", "amg-potential"]: - if self.options.get("linear_solver_verbosity", False): - num_amg_iter = len(self.res_history_amg) - res_amg = self.res_history_amg[-1] - print(ml) - print( - f"#AMG iterations: {num_amg_iter}; Residual after AMG step: {res_amg}" - ) - - # Collect stats - stats = [time_setup, time_solve] - - return update, residual, stats - def _solve(self, flat_mass_diff: np.ndarray) -> tuple[float, np.ndarray, dict]: """Solve the Beckman problem using Newton's method. @@ -907,7 +907,6 @@ def _solve(self, flat_mass_diff: np.ndarray) -> tuple[float, np.ndarray, dict]: # Setup tic = time.time() - self.setup_infrastructure() # Solver parameters num_iter = self.options.get("num_iter", 100) @@ -958,10 +957,33 @@ def _solve(self, flat_mass_diff: np.ndarray) -> tuple[float, np.ndarray, dict]: old_flux = solution_i[self.flux_slice] old_distance = self.l1_dissipation(old_flux, "cell_arithmetic") - # Newton step - update_i, residual_i, stats_i = self.linearization_step( - solution_i, rhs, iter - ) + # Assemble linear problem in Newton step + tic = time.time() + if iter == 0: + # Determine residual and (full) Jacobian of a linear Darcy problem + residual_i = rhs.copy() + approx_jacobian = self.darcy_init.copy() + else: + # Determine residual and (full) Jacobian + residual_i = self.residual(rhs, solution_i) + approx_jacobian = self.jacobian(solution_i) + toc = time.time() + time_assemble = toc - tic + + # Solve linear system for the update + update_i, stats_i = self.linear_solve(approx_jacobian, residual_i, solution_i) + + # Diagnostics + # TODO move? + if self.linear_solver in ["amg-flux-reduced", "amg-potential"]: + if self.options.get("linear_solver_verbosity", False): + # print(ml) # TODO rm? + print( + f"""#AMG iterations: {stats_i["amg num iterations"]}; """ + f"""Residual after AMG step: {stats_i["amg residual"]}""" + ) + + # Update the solution with the full Netwon step solution_i += update_i # Apply Anderson acceleration to flux contribution (the only nonlinear part). @@ -976,7 +998,10 @@ def _solve(self, flat_mass_diff: np.ndarray) -> tuple[float, np.ndarray, dict]: ) toc = time.time() time_anderson = toc - tic - stats_i.append(time_anderson) + + # Update stats + stats_i["time assemble"] = time_assemble + stats_i["time acceleration"] = time_anderson # Update distance new_flux = solution_i[self.flux_slice] @@ -1133,9 +1158,6 @@ def _solve(self, flat_mass_diff): tol_increment = self.options.get("tol_increment", 1e-6) tol_distance = self.options.get("tol_distance", 1e-6) - # Define linear solver to be used to invert the Darcy systems - self.setup_infrastructure() - # Relaxation parameter self.L = self.options.get("L", 1.0) rhs = np.concatenate( @@ -1241,7 +1263,7 @@ def _solve(self, flat_mass_diff): self.fully_reduced_jacobian, self.fully_reduced_residual, ) = self.remove_lagrange_multiplier( - self.reduced_jacobian, self.reduced_residual, solution_i + self.reduced_jacobian, self.reduced_residual ) if self.linear_solver == "lu-potential": @@ -1463,7 +1485,7 @@ def _solve(self, flat_mass_diff): self.fully_reduced_jacobian, self.fully_reduced_residual, ) = self.remove_lagrange_multiplier( - self.reduced_jacobian, self.reduced_residual, solution_i + self.reduced_jacobian, self.reduced_residual ) if self.linear_solver == "lu-potential": From bd2da63aebbef5e53a69da66653a4ea0153bc840 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Mon, 18 Sep 2023 12:44:47 +0200 Subject: [PATCH 067/100] MAINT: rename attribute --- src/darsia/measure/wasserstein.py | 66 ++++++++++++++++--------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index cd6c3c48..139caa20 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -191,16 +191,16 @@ def _setup_discretization(self) -> None: """sps.csc_matrix: initial Darcy operator""" def _setup_linear_solver(self) -> None: - self.linear_solver = self.options.get("linear_solver", "lu") - assert self.linear_solver in [ + self.linear_solver_type = self.options.get("linear_solver", "lu") + assert self.linear_solver_type in [ "lu", "lu-flux-reduced", "amg-flux-reduced", "lu-potential", "amg-potential", - ], f"Linear solver {self.linear_solver} not supported." + ], f"Linear solver {self.linear_solver_type} not supported." - if self.linear_solver in ["amg-flux-reduced", "amg-potential"]: + if self.linear_solver_type in ["amg-flux-reduced", "amg-potential"]: # TODO add possibility for user control self.ml_options = { # B=X.reshape( @@ -228,9 +228,9 @@ def _setup_linear_solver(self) -> None: self.res_history_amg = [] # Setup inrastructure for Schur complement reduction - if self.linear_solver in ["lu-flux-reduced", "amg-flux-reduced"]: + if self.linear_solver_type in ["lu-flux-reduced", "amg-flux-reduced"]: self.setup_one_level_schur_reduction() - elif self.linear_solver in ["lu-potential", "amg-potential"]: + elif self.linear_solver_type in ["lu-potential", "amg-potential"]: self.setup_two_level_schur_reduction() def setup_one_level_schur_reduction(self) -> None: @@ -459,7 +459,7 @@ def linear_solve( rhs: np.ndarray, previous_solution: Optional[np.ndarray] = None, ): - if self.linear_solver == "lu": + if self.linear_solver_type == "lu": # Setup LU factorization for the full system tic = time.time() lu = sps.linalg.splu(matrix) @@ -470,7 +470,7 @@ def linear_solve( solution = lu.solve(rhs) time_solve = time.time() - tic - elif self.linear_solver in [ + elif self.linear_solver_type in [ "lu-flux-reduced", "amg-flux-reduced", "lu-potential", @@ -489,7 +489,7 @@ def linear_solve( matrix_flux_inv, ) = self.remove_flux(matrix, rhs) - if self.linear_solver == "lu-flux-reduced": + if self.linear_solver_type == "lu-flux-reduced": # LU factorization for reduced system lu = sps.linalg.splu(self.reduced_matrix) time_setup = time.time() - tic @@ -498,7 +498,7 @@ def linear_solve( tic = time.time() solution[self.reduced_system_slice] = lu.solve(self.reduced_rhs) - elif self.linear_solver == "amg-flux-reduced": + elif self.linear_solver_type == "amg-flux-reduced": # AMG solver for reduced system ml = pyamg.smoothed_aggregation_solver( self.reduced_matrix, **self.ml_options @@ -539,7 +539,7 @@ def linear_solve( self.reduced_rhs, ) - if self.linear_solver == "lu-potential": + if self.linear_solver_type == "lu-potential": # Finish LU factorization of the pure potential system lu = sps.linalg.splu(self.fully_reduced_matrix) time_setup = time.time() - tic @@ -550,7 +550,7 @@ def linear_solve( self.fully_reduced_rhs ) - elif self.linear_solver == "amg-potential": + elif self.linear_solver_type == "amg-potential": # Finish AMG setup of th pure potential system ml = pyamg.smoothed_aggregation_solver( self.fully_reduced_jacobian, **self.ml_options @@ -575,7 +575,7 @@ def linear_solve( "time setup": time_setup, "time solve": time_solve, } - if self.linear_solver in ["amg-flux-reduced", "amg-potential"]: + if self.linear_solver_type in ["amg-flux-reduced", "amg-potential"]: stats["amg residuals"] = self.res_history_amg stats["amg num iterations"] = len(self.res_history_amg) stats["amg residual"] = self.res_history_amg[-1] @@ -971,11 +971,13 @@ def _solve(self, flat_mass_diff: np.ndarray) -> tuple[float, np.ndarray, dict]: time_assemble = toc - tic # Solve linear system for the update - update_i, stats_i = self.linear_solve(approx_jacobian, residual_i, solution_i) + update_i, stats_i = self.linear_solve( + approx_jacobian, residual_i, solution_i + ) # Diagnostics # TODO move? - if self.linear_solver in ["amg-flux-reduced", "amg-potential"]: + if self.linear_solver_type in ["amg-flux-reduced", "amg-potential"]: if self.options.get("linear_solver_verbosity", False): # print(ml) # TODO rm? print( @@ -1211,11 +1213,11 @@ def _solve(self, flat_mass_diff): ], format="csc", ) - if self.linear_solver == "lu": + if self.linear_solver_type == "lu": self.l_scheme_mixed_darcy_solver = sps.linalg.splu(l_scheme_mixed_darcy) solution_i = self.l_scheme_mixed_darcy_solver.solve(rhs) - elif self.linear_solver in ["lu-flux-reduced", "amg-flux-reduced"]: + elif self.linear_solver_type in ["lu-flux-reduced", "amg-flux-reduced"]: # Solve potential-multiplier problem # Reduce flux block @@ -1225,12 +1227,12 @@ def _solve(self, flat_mass_diff): jacobian_flux_inv, ) = self.remove_flux(l_scheme_mixed_darcy, rhs) - if self.linear_solver == "lu-flux-reduced": + if self.linear_solver_type == "lu-flux-reduced": self.l_scheme_mixed_darcy_solver = sps.linalg.splu( self.reduced_jacobian ) - elif self.linear_solver == "amg-flux-reduced": + elif self.linear_solver_type == "amg-flux-reduced": self.l_scheme_mixed_darcy_solver = pyamg.smoothed_aggregation_solver( self.reduced_jacobian, **self.ml_options ) @@ -1248,7 +1250,7 @@ def _solve(self, flat_mass_diff): + self.DT.dot(solution_i[self.reduced_system_slice]) ) - elif self.linear_solver in ["lu-potential", "amg-potential"]: + elif self.linear_solver_type in ["lu-potential", "amg-potential"]: # Solve pure potential problem # Reduce flux block @@ -1266,7 +1268,7 @@ def _solve(self, flat_mass_diff): self.reduced_jacobian, self.reduced_residual ) - if self.linear_solver == "lu-potential": + if self.linear_solver_type == "lu-potential": self.l_scheme_mixed_darcy_solver = sps.linalg.splu( self.fully_reduced_jacobian ) @@ -1274,7 +1276,7 @@ def _solve(self, flat_mass_diff): self.fully_reduced_system_indices_full ] = self.l_scheme_mixed_darcy_solver.solve(self.fully_reduced_residual) - elif self.linear_solver == "amg-potential": + elif self.linear_solver_type == "amg-potential": self.l_scheme_mixed_darcy_solver = pyamg.smoothed_aggregation_solver( self.fully_reduced_jacobian, **self.ml_options ) @@ -1294,7 +1296,7 @@ def _solve(self, flat_mass_diff): else: raise NotImplementedError( - f"Linear solver {self.linear_solver} not supported" + f"Linear solver {self.linear_solver_type} not supported" ) # Extract intial values @@ -1421,14 +1423,14 @@ def _solve(self, flat_mass_diff): ) solution_i = np.zeros_like(rhs_i, dtype=float) - if self.linear_solver == "lu": + if self.linear_solver_type == "lu": if update_solver: self.l_scheme_mixed_darcy_solver = sps.linalg.splu( l_scheme_mixed_darcy ) solution_i = self.l_scheme_mixed_darcy_solver.solve(rhs_i) - elif self.linear_solver in ["lu-flux-reduced", "amg-flux-reduced"]: + elif self.linear_solver_type in ["lu-flux-reduced", "amg-flux-reduced"]: # Solve potential-multiplier problem # Reduce flux block @@ -1438,7 +1440,7 @@ def _solve(self, flat_mass_diff): jacobian_flux_inv, ) = self.remove_flux(l_scheme_mixed_darcy, rhs_i) - if self.linear_solver == "lu-flux-reduced": + if self.linear_solver_type == "lu-flux-reduced": if update_solver: self.l_scheme_mixed_darcy_solver = sps.linalg.splu( self.reduced_jacobian @@ -1449,7 +1451,7 @@ def _solve(self, flat_mass_diff): self.reduced_residual ) - elif self.linear_solver == "amg-flux-reduced": + elif self.linear_solver_type == "amg-flux-reduced": if update_solver: self.l_scheme_mixed_darcy_solver = ( pyamg.smoothed_aggregation_solver( @@ -1470,7 +1472,7 @@ def _solve(self, flat_mass_diff): + self.DT.dot(solution_i[self.reduced_system_slice]) ) - elif self.linear_solver in ["lu-potential", "amg-potential"]: + elif self.linear_solver_type in ["lu-potential", "amg-potential"]: # Solve pure potential problem # Reduce flux block @@ -1488,7 +1490,7 @@ def _solve(self, flat_mass_diff): self.reduced_jacobian, self.reduced_residual ) - if self.linear_solver == "lu-potential": + if self.linear_solver_type == "lu-potential": if update_solver: self.l_scheme_mixed_darcy_solver = sps.linalg.splu( self.fully_reduced_jacobian @@ -1499,7 +1501,7 @@ def _solve(self, flat_mass_diff): self.fully_reduced_residual ) - elif self.linear_solver == "amg-potential": + elif self.linear_solver_type == "amg-potential": if update_solver: self.l_scheme_mixed_darcy_solver = ( pyamg.smoothed_aggregation_solver( @@ -1523,11 +1525,11 @@ def _solve(self, flat_mass_diff): ) else: raise NotImplementedError( - f"""Linear solver {self.linear_solver} not supported.""" + f"""Linear solver {self.linear_solver_type} not supported.""" ) # Diagnostics - if self.linear_solver in ["amg-flux-reduced", "amg-potential"]: + if self.linear_solver_type in ["amg-flux-reduced", "amg-potential"]: if self.options.get("linear_solver_verbosity", False): num_amg_iter = len(self.res_history_amg) res_amg = self.res_history_amg[-1] From 182a26b97751cbf66b44e94fd16943a39060ad22 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Mon, 18 Sep 2023 15:23:10 +0200 Subject: [PATCH 068/100] MAINT: Reorganize and bundle linear solution. --- src/darsia/measure/wasserstein.py | 273 +++++++----------------------- 1 file changed, 57 insertions(+), 216 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index 139caa20..f783c026 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -458,16 +458,34 @@ def linear_solve( matrix: sps.csc_matrix, rhs: np.ndarray, previous_solution: Optional[np.ndarray] = None, - ): + reuse_solver: bool = False, + ) -> tuple: + """Solve the linear system. + + For reusing the setup, the resulting solver is cached as self.linear_solver. + + Args: + matrix (sps.csc_matrix): matrix + rhs (np.ndarray): right hand side + previous_solution (np.ndarray): previous solution. Defaults to None. + + Returns: + tuple: solution, stats + + """ + + setup_linear_solver = not (reuse_solver) or not (hasattr(self, "linear_solver")) + if self.linear_solver_type == "lu": # Setup LU factorization for the full system tic = time.time() - lu = sps.linalg.splu(matrix) + if setup_linear_solver: + self.linear_solver = sps.linalg.splu(matrix) time_setup = time.time() - tic # Solve the full system tic = time.time() - solution = lu.solve(rhs) + solution = self.linear_solver.solve(rhs) time_solve = time.time() - tic elif self.linear_solver_type in [ @@ -491,23 +509,27 @@ def linear_solve( if self.linear_solver_type == "lu-flux-reduced": # LU factorization for reduced system - lu = sps.linalg.splu(self.reduced_matrix) + if setup_linear_solver: + self.linear_solver = sps.linalg.splu(self.reduced_matrix) time_setup = time.time() - tic # Solve for the potential and lagrange multiplier tic = time.time() - solution[self.reduced_system_slice] = lu.solve(self.reduced_rhs) + solution[self.reduced_system_slice] = self.linear_solver.solve( + self.reduced_rhs + ) elif self.linear_solver_type == "amg-flux-reduced": # AMG solver for reduced system - ml = pyamg.smoothed_aggregation_solver( - self.reduced_matrix, **self.ml_options - ) + if setup_linear_solver: + self.linear_solver = pyamg.smoothed_aggregation_solver( + self.reduced_matrix, **self.ml_options + ) time_setup = time.time() - tic # Solve for the potential and lagrange multiplier tic = time.time() - solution[self.reduced_system_slice] = ml.solve( + solution[self.reduced_system_slice] = self.linear_solver.solve( self.reduced_rhs, tol=self.tol_amg, residuals=self.res_history_amg, @@ -518,7 +540,7 @@ def linear_solve( # NOTE: It is implicitly assumed that the lagrange multiplier is zero # in the constrained cell. This is not checked here. And no update is # performed. - if ( + if previous_solution is not None and ( abs( previous_solution[ self.grid.num_faces + self.constrained_cell_flat_index @@ -541,25 +563,29 @@ def linear_solve( if self.linear_solver_type == "lu-potential": # Finish LU factorization of the pure potential system - lu = sps.linalg.splu(self.fully_reduced_matrix) + if setup_linear_solver: + self.linear_solver = sps.linalg.splu(self.fully_reduced_matrix) time_setup = time.time() - tic # Solve the pure potential system tic = time.time() - solution[self.fully_reduced_system_indices_full] = lu.solve( - self.fully_reduced_rhs - ) + solution[ + self.fully_reduced_system_indices_full + ] = self.linear_solver.solve(self.fully_reduced_rhs) elif self.linear_solver_type == "amg-potential": # Finish AMG setup of th pure potential system - ml = pyamg.smoothed_aggregation_solver( - self.fully_reduced_jacobian, **self.ml_options - ) + if setup_linear_solver: + self.linear_solver = pyamg.smoothed_aggregation_solver( + self.fully_reduced_jacobian, **self.ml_options + ) time_setup = time.time() - tic # Solve the pure potential system tic = time.time() - solution[self.fully_reduced_system_indices_full] = ml.solve( + solution[ + self.fully_reduced_system_indices_full + ] = self.linear_solver.solve( self.fully_reduced_rhs, tol=self.tol_amg, residuals=self.res_history_amg, @@ -1202,7 +1228,6 @@ def _solve(self, flat_mass_diff): dissipation_mode = "cell_arithmetic" weight = self.L shrink_factor = 1.0 / self.L - solution_i = np.zeros_like(rhs, dtype=float) # Solve linear Darcy problem as initial guess l_scheme_mixed_darcy = sps.bmat( @@ -1213,91 +1238,8 @@ def _solve(self, flat_mass_diff): ], format="csc", ) - if self.linear_solver_type == "lu": - self.l_scheme_mixed_darcy_solver = sps.linalg.splu(l_scheme_mixed_darcy) - solution_i = self.l_scheme_mixed_darcy_solver.solve(rhs) - - elif self.linear_solver_type in ["lu-flux-reduced", "amg-flux-reduced"]: - # Solve potential-multiplier problem - - # Reduce flux block - ( - self.reduced_jacobian, - self.reduced_residual, - jacobian_flux_inv, - ) = self.remove_flux(l_scheme_mixed_darcy, rhs) - - if self.linear_solver_type == "lu-flux-reduced": - self.l_scheme_mixed_darcy_solver = sps.linalg.splu( - self.reduced_jacobian - ) - - elif self.linear_solver_type == "amg-flux-reduced": - self.l_scheme_mixed_darcy_solver = pyamg.smoothed_aggregation_solver( - self.reduced_jacobian, **self.ml_options - ) - solution_i[ - self.reduced_system_slice - ] = self.l_scheme_mixed_darcy_solver.solve( - self.reduced_residual, - tol=self.tol_amg, - residuals=self.res_history_amg, - ) - - # Compute flux update - solution_i[self.flux_slice] = jacobian_flux_inv.dot( - rhs[self.flux_slice] - + self.DT.dot(solution_i[self.reduced_system_slice]) - ) - - elif self.linear_solver_type in ["lu-potential", "amg-potential"]: - # Solve pure potential problem - - # Reduce flux block - ( - self.reduced_jacobian, - self.reduced_residual, - jacobian_flux_inv, - ) = self.remove_flux(l_scheme_mixed_darcy, rhs) - - # Reduce to pure pressure system - ( - self.fully_reduced_jacobian, - self.fully_reduced_residual, - ) = self.remove_lagrange_multiplier( - self.reduced_jacobian, self.reduced_residual - ) - - if self.linear_solver_type == "lu-potential": - self.l_scheme_mixed_darcy_solver = sps.linalg.splu( - self.fully_reduced_jacobian - ) - solution_i[ - self.fully_reduced_system_indices_full - ] = self.l_scheme_mixed_darcy_solver.solve(self.fully_reduced_residual) - - elif self.linear_solver_type == "amg-potential": - self.l_scheme_mixed_darcy_solver = pyamg.smoothed_aggregation_solver( - self.fully_reduced_jacobian, **self.ml_options - ) - solution_i[ - self.fully_reduced_system_indices_full - ] = self.l_scheme_mixed_darcy_solver.solve( - self.fully_reduced_residual, - tol=self.tol_amg, - residuals=self.res_history_amg, - ) - - # Compute flux update - solution_i[self.flux_slice] = jacobian_flux_inv.dot( - rhs[self.flux_slice] - + self.DT.dot(solution_i[self.reduced_system_slice]) - ) - - else: - raise NotImplementedError( - f"Linear solver {self.linear_solver_type} not supported" - ) + solution_i = np.zeros_like(rhs, dtype=float) + solution_i, _ = self.linear_solve(l_scheme_mixed_darcy, rhs, solution_i) # Extract intial values old_flux = solution_i[self.flux_slice] @@ -1316,9 +1258,10 @@ def _solve(self, flat_mass_diff): rhs_i[self.flux_slice] = self.L * self.mass_matrix_faces.dot( old_aux_flux - old_force ) - new_flux = self.l_scheme_mixed_darcy_solver.solve(rhs_i)[ - self.flux_slice - ] + solution_i, _ = self.linear_solve( + l_scheme_mixed_darcy, rhs_i, reuse_solver=True + ) + new_flux = solution_i[self.flux_slice] time_linearization = time.time() - tic # 2. Shrink step for vectorial fluxes. To comply with the RT0 setting, the @@ -1368,9 +1311,10 @@ def _solve(self, flat_mass_diff): rhs_i[self.flux_slice] = self.L * self.mass_matrix_faces.dot( new_aux_flux - new_force ) - new_flux = self.l_scheme_mixed_darcy_solver.solve(rhs_i)[ - self.flux_slice - ] + solution_i, _ = self.linear_solve( + l_scheme_mixed_darcy, rhs_i, reuse_solver=True + ) + new_flux = solution_i[self.flux_slice] time_linearization = time.time() - tic # Apply Anderson acceleration to flux contribution (the only nonlinear part). @@ -1421,112 +1365,9 @@ def _solve(self, flat_mass_diff): rhs_i[self.flux_slice] = weight * self.mass_matrix_faces.dot( old_aux_flux - old_force ) - solution_i = np.zeros_like(rhs_i, dtype=float) - - if self.linear_solver_type == "lu": - if update_solver: - self.l_scheme_mixed_darcy_solver = sps.linalg.splu( - l_scheme_mixed_darcy - ) - solution_i = self.l_scheme_mixed_darcy_solver.solve(rhs_i) - - elif self.linear_solver_type in ["lu-flux-reduced", "amg-flux-reduced"]: - # Solve potential-multiplier problem - - # Reduce flux block - ( - self.reduced_jacobian, - self.reduced_residual, - jacobian_flux_inv, - ) = self.remove_flux(l_scheme_mixed_darcy, rhs_i) - - if self.linear_solver_type == "lu-flux-reduced": - if update_solver: - self.l_scheme_mixed_darcy_solver = sps.linalg.splu( - self.reduced_jacobian - ) - solution_i[ - self.reduced_system_slice - ] = self.l_scheme_mixed_darcy_solver.solve( - self.reduced_residual - ) - - elif self.linear_solver_type == "amg-flux-reduced": - if update_solver: - self.l_scheme_mixed_darcy_solver = ( - pyamg.smoothed_aggregation_solver( - self.reduced_jacobian, **self.ml_options - ) - ) - solution_i[ - self.reduced_system_slice - ] = self.l_scheme_mixed_darcy_solver.solve( - self.reduced_residual, - tol=self.tol_amg, - residuals=self.res_history_amg, - ) - - # Compute flux update - solution_i[self.flux_slice] = jacobian_flux_inv.dot( - rhs_i[self.flux_slice] - + self.DT.dot(solution_i[self.reduced_system_slice]) - ) - - elif self.linear_solver_type in ["lu-potential", "amg-potential"]: - # Solve pure potential problem - - # Reduce flux block - ( - self.reduced_jacobian, - self.reduced_residual, - jacobian_flux_inv, - ) = self.remove_flux(l_scheme_mixed_darcy, rhs_i) - - # Reduce to pure pressure system - ( - self.fully_reduced_jacobian, - self.fully_reduced_residual, - ) = self.remove_lagrange_multiplier( - self.reduced_jacobian, self.reduced_residual - ) - - if self.linear_solver_type == "lu-potential": - if update_solver: - self.l_scheme_mixed_darcy_solver = sps.linalg.splu( - self.fully_reduced_jacobian - ) - solution_i[ - self.fully_reduced_system_indices_full - ] = self.l_scheme_mixed_darcy_solver.solve( - self.fully_reduced_residual - ) - - elif self.linear_solver_type == "amg-potential": - if update_solver: - self.l_scheme_mixed_darcy_solver = ( - pyamg.smoothed_aggregation_solver( - self.fully_reduced_jacobian, **self.ml_options - ) - ) - # time_setup = time.time() - tic - tic = time.time() - solution_i[ - self.fully_reduced_system_indices_full - ] = self.l_scheme_mixed_darcy_solver.solve( - self.fully_reduced_residual, - tol=self.tol_amg, - residuals=self.res_history_amg, - ) - - # Compute flux update - solution_i[self.flux_slice] = jacobian_flux_inv.dot( - rhs_i[self.flux_slice] - + self.DT.dot(solution_i[self.reduced_system_slice]) - ) - else: - raise NotImplementedError( - f"""Linear solver {self.linear_solver_type} not supported.""" - ) + solution_i, _ = self.linear_solve( + l_scheme_mixed_darcy, rhs_i, reuse_solver=not (update_solver) + ) # Diagnostics if self.linear_solver_type in ["amg-flux-reduced", "amg-potential"]: From 04a1246e6c46df14040df55b562d7c856f913f40 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Mon, 18 Sep 2023 19:06:08 +0200 Subject: [PATCH 069/100] DOC: Add better documentation of attributes --- src/darsia/measure/wasserstein.py | 43 ++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index f783c026..f227c369 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -62,13 +62,19 @@ def __init__( """ # Cache geometrical infos self.grid = grid - self.voxel_size = grid.voxel_size + """darsia.Grid: grid""" - assert self.grid.dim == 2, "Currently only 2D images are supported." + self.voxel_size = grid.voxel_size + """np.ndarray: voxel size""" self.options = options + """dict: options for the solver""" + self.regularization = self.options.get("regularization", 0.0) + """float: regularization parameter""" + self.verbose = self.options.get("verbose", False) + """bool: verbosity""" # Setup of method self._setup_dof_management() @@ -142,6 +148,8 @@ def _setup_discretization(self) -> None: self.constrained_cell_flat_index = np.ravel_multi_index( center_cell, self.grid.shape ) + """int: flat index of the cell where the potential is constrained to zero""" + num_potential_dofs = self.grid.num_cells self.potential_constraint = sps.csc_matrix( ( @@ -199,6 +207,7 @@ def _setup_linear_solver(self) -> None: "lu-potential", "amg-potential", ], f"Linear solver {self.linear_solver_type} not supported." + """str: type of linear solver""" if self.linear_solver_type in ["amg-flux-reduced", "amg-potential"]: # TODO add possibility for user control @@ -224,12 +233,18 @@ def _setup_linear_solver(self) -> None: "max_coarse": 1000, # maximum number on a coarse level # keep=False, # keep extra operators around in the hierarchy (memory) } + """dict: options for the AMG solver""" + self.tol_amg = self.options.get("linear_solver_tol", 1e-6) + """float: tolerance for the AMG solver""" + self.res_history_amg = [] + """list: history of residuals for the AMG solver""" # Setup inrastructure for Schur complement reduction if self.linear_solver_type in ["lu-flux-reduced", "amg-flux-reduced"]: self.setup_one_level_schur_reduction() + elif self.linear_solver_type in ["lu-potential", "amg-potential"]: self.setup_two_level_schur_reduction() @@ -241,7 +256,6 @@ def setup_one_level_schur_reduction(self) -> None: """ # Step 1: Compute the jacobian of the Darcy problem - # The Darcy problem is sufficient jacobian = self.darcy_init.copy() # Step 2: Remove flux blocks through Schur complement approach @@ -253,16 +267,21 @@ def setup_one_level_schur_reduction(self) -> None: # Cache divergence matrix self.D = D.copy() + """sps.csc_matrix: divergence matrix""" + self.DT = self.D.T.copy() + """sps.csc_matrix: transposed divergence matrix""" # Cache (constant) jacobian subblock self.jacobian_subblock = jacobian[ self.reduced_system_slice, self.reduced_system_slice ].copy() + """sps.csc_matrix: constant jacobian subblock of the reduced system""" # Add Schur complement - use this to identify sparsity structure # Cache the reduced jacobian self.reduced_jacobian = self.jacobian_subblock + schur_complement + """sps.csc_matrix: reduced jacobian incl. Schur complement""" def setup_two_level_schur_reduction(self) -> None: """Additional setup of infrastructure for fully reduced systems.""" @@ -288,6 +307,7 @@ def setup_two_level_schur_reduction(self) -> None: ) # Cache for later use in remove_lagrange_multiplier self.rm_indices = rm_indices + """np.ndarray: indices to be removed in the reduced system""" # Identify rows to be reduced rm_rows = [ @@ -332,11 +352,17 @@ def setup_two_level_schur_reduction(self) -> None: ), shape=fully_reduced_jacobian_shape, ) + """sps.csc_matrix: fully reduced jacobian""" # Cache the indices and indptr self.fully_reduced_jacobian_indices = fully_reduced_jacobian_indices.copy() + """np.ndarray: indices of the fully reduced jacobian""" + self.fully_reduced_jacobian_indptr = fully_reduced_jacobian_indptr.copy() + """np.ndarray: indptr of the fully reduced jacobian""" + self.fully_reduced_jacobian_shape = fully_reduced_jacobian_shape + """tuple: shape of the fully reduced jacobian""" # Step 4: Identify inclusions (index arrays) @@ -350,11 +376,13 @@ def setup_two_level_schur_reduction(self) -> None: self.fully_reduced_system_indices = np.delete( np.arange(self.grid.num_cells), self.constrained_cell_flat_index ) + """np.ndarray: indices of the fully reduced system in terms of reduced system""" # Define fully reduced system indices wrt full system self.fully_reduced_system_indices_full = reduced_system_indices[ self.fully_reduced_system_indices ] + """np.ndarray: indices of the fully reduced system in terms of full system""" def _setup_acceleration(self) -> None: """Setup of acceleration methods.""" @@ -746,7 +774,14 @@ def _plot_solution( potential (np.ndarray): potential transport_density (np.ndarray): transport density + Raises: + NotImplementedError: plotting only implemented for 2D + """ + + if self.grid.dim != 2: + raise NotImplementedError("Plotting only implemented for 2D.") + # Fetch options plot_options = self.options.get("plot_options", {}) name = plot_options.get("name", None) @@ -1007,7 +1042,7 @@ def _solve(self, flat_mass_diff: np.ndarray) -> tuple[float, np.ndarray, dict]: if self.options.get("linear_solver_verbosity", False): # print(ml) # TODO rm? print( - f"""#AMG iterations: {stats_i["amg num iterations"]}; """ + f"""AMG iterations: {stats_i["amg num iterations"]}; """ f"""Residual after AMG step: {stats_i["amg residual"]}""" ) From 8107c60198bef941ab7a3b233c5492240c236eba Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Mon, 18 Sep 2023 19:44:17 +0200 Subject: [PATCH 070/100] TST: Extend Wasserstein test - add more solvers, prepare for 3d --- tests/unit/test_wasserstein.py | 119 +++++++++++++++++++++++++-------- 1 file changed, 92 insertions(+), 27 deletions(-) diff --git a/tests/unit/test_wasserstein.py b/tests/unit/test_wasserstein.py index 4168cc7d..379ed821 100644 --- a/tests/unit/test_wasserstein.py +++ b/tests/unit/test_wasserstein.py @@ -5,28 +5,71 @@ import darsia +# ! ---- 2d version ---- + # Coarse src image rows = 10 cols = rows -src_square = np.zeros((rows, cols), dtype=float) -src_square[2:5, 2:5] = 1 -meta = {"width": 1, "height": 1, "space_dim": 2, "scalar": True} -src_image = darsia.Image(src_square, **meta) +src_square_2d = np.zeros((rows, cols), dtype=float) +src_square_2d[2:5, 2:5] = 1 +meta_2d = {"width": 1, "height": 1, "dim": 2, "scalar": True} +src_image_2d = darsia.Image(src_square_2d, **meta_2d) + +# Coarse dst image +dst_squares_2d = np.zeros((rows, cols), dtype=float) +dst_squares_2d[1:3, 1:2] = 1 +dst_squares_2d[4:7, 7:9] = 1 +dst_image_2d = darsia.Image(dst_squares_2d, **meta_2d) + +# Rescale +shape_meta_2d = src_image_2d.shape_metadata() +geometry_2d = darsia.Geometry(**shape_meta_2d) +src_image_2d.img /= geometry_2d.integrate(src_image_2d) +dst_image_2d.img /= geometry_2d.integrate(dst_image_2d) + +# Reference value for comparison +true_distance_2d = 0.379543951823 + +# ! ---- 3d version ---- + +# Coarse src image +src_square_3d = np.zeros((rows, cols, 1), dtype=float) +src_square_3d[2:5, 2:5, 0] = 1 +meta_3d = {"dimensions": [1, 1, 1], "dim": 3, "series": False, "scalar": True} +src_image_3d = darsia.Image(src_square_3d, **meta_3d) # Coarse dst image -dst_squares = np.zeros((rows, cols), dtype=float) -dst_squares[1:3, 1:2] = 1 -dst_squares[4:7, 7:9] = 1 -dst_image = darsia.Image(dst_squares, **meta) +dst_squares_3d = np.zeros((rows, cols, 1), dtype=float) +dst_squares_3d[1:3, 1:2, 0] = 1 +dst_squares_3d[4:7, 7:9, 0] = 1 +dst_image_3d = darsia.Image(dst_squares_3d, **meta_3d) # Rescale -shape_meta = src_image.shape_metadata() -geometry = darsia.Geometry(**shape_meta) -src_image.img /= geometry.integrate(src_image) -dst_image.img /= geometry.integrate(dst_image) +shape_meta_3d = src_image_3d.shape_metadata() +geometry_3d = darsia.Geometry(**shape_meta_3d) +src_image_3d.img /= geometry_3d.integrate(src_image_3d) +dst_image_3d.img /= geometry_3d.integrate(dst_image_3d) # Reference value for comparison -true_distance = 0.379543951823 +true_distance_3d = 0.379543951823 + +# ! ---- Data set ---- +src_image = { + 2: src_image_2d, + 3: src_image_3d, +} + +dst_image = { + 2: dst_image_2d, + 3: dst_image_3d, +} + +true_distance = { + 2: true_distance_2d, + 3: true_distance_3d, +} + +# ! ---- Solver options ---- # Linearization newton_options = { @@ -96,58 +139,80 @@ "verbose": False, } +# ! ---- Tests ---- + @pytest.mark.parametrize("a_key", range(len(accelerations))) @pytest.mark.parametrize("s_key", range(len(solvers))) -def test_newton(a_key, s_key): +@pytest.mark.parametrize("dim", [2]) +def test_newton(a_key, s_key, dim): """Test all combinations for Newton.""" options.update(newton_options) options.update(accelerations[a_key]) options.update(solvers[s_key]) distance, _, _, _, status = darsia.wasserstein_distance( - src_image, dst_image, options=options, method="newton", return_solution=True + src_image[dim], + dst_image[dim], + options=options, + method="newton", + return_solution=True, ) - assert np.isclose(distance, true_distance, atol=1e-5) + assert np.isclose(distance, true_distance[dim], atol=1e-5) assert status["converged"] @pytest.mark.parametrize("a_key", range(len(accelerations))) -@pytest.mark.parametrize("s_key", [0]) # TODO range(len(solvers))) -def test_std_bregman(a_key, s_key): +@pytest.mark.parametrize("s_key", range(len(solvers))) +@pytest.mark.parametrize("dim", [2]) +def test_std_bregman(a_key, s_key, dim): """Test all combinations for std Bregman.""" options.update(bregman_std_options) options.update(accelerations[a_key]) options.update(solvers[s_key]) distance, _, _, _, status = darsia.wasserstein_distance( - src_image, dst_image, options=options, method="bregman", return_solution=True + src_image[dim], + dst_image[dim], + options=options, + method="bregman", + return_solution=True, ) - assert np.isclose(distance, true_distance, atol=1e-2) # TODO + assert np.isclose(distance, true_distance[dim], atol=1e-2) # TODO assert status["converged"] @pytest.mark.parametrize("a_key", range(len(accelerations))) -@pytest.mark.parametrize("s_key", [0]) # TODO range(len(solvers))) -def test_reordered_bregman(a_key, s_key): +@pytest.mark.parametrize("s_key", range(len(solvers))) +@pytest.mark.parametrize("dim", [2]) +def test_reordered_bregman(a_key, s_key, dim): """Test all combinations for reordered Bregman.""" options.update(bregman_reordered_options) options.update(accelerations[a_key]) options.update(solvers[s_key]) distance, _, _, _, status = darsia.wasserstein_distance( - src_image, dst_image, options=options, method="bregman", return_solution=True + src_image[dim], + dst_image[dim], + options=options, + method="bregman", + return_solution=True, ) - assert np.isclose(distance, true_distance, atol=1e-2) # TODO + assert np.isclose(distance, true_distance[dim], atol=1e-2) # TODO assert status["converged"] @pytest.mark.parametrize("a_key", range(len(accelerations))) @pytest.mark.parametrize("s_key", range(len(solvers))) -def test_adaptive_bregman(a_key, s_key): +@pytest.mark.parametrize("dim", [2]) +def test_adaptive_bregman(a_key, s_key, dim): """Test all combinations for adaptive Bregman.""" options.update(bregman_adaptive_options) options.update(accelerations[a_key]) options.update(solvers[s_key]) distance, _, _, _, status = darsia.wasserstein_distance( - src_image, dst_image, options=options, method="bregman", return_solution=True + src_image[dim], + dst_image[dim], + options=options, + method="bregman", + return_solution=True, ) - assert np.isclose(distance, true_distance, atol=1e-5) + assert np.isclose(distance, true_distance[dim], atol=1e-5) assert status["converged"] From c56d12b27cf32c831a05f1d7d88e56e333658eef Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Tue, 19 Sep 2023 12:15:35 +0200 Subject: [PATCH 071/100] MAINT: Generalize grid as preparation for extension to 3d. --- src/darsia/utils/grid.py | 275 ++++++++++++++++++++++++++++----------- 1 file changed, 200 insertions(+), 75 deletions(-) diff --git a/src/darsia/utils/grid.py b/src/darsia/utils/grid.py index c137bea3..e6d61bba 100644 --- a/src/darsia/utils/grid.py +++ b/src/darsia/utils/grid.py @@ -1,4 +1,4 @@ -"""Grid utilities.""" +"""Grid utilities for tensor grids.""" from typing import Union @@ -6,6 +6,8 @@ import darsia +# TODO make nested lits to arrays for faster access. + class Grid: """Tensor grid. @@ -22,12 +24,18 @@ def __init__(self, shape: tuple, voxel_size: Union[float, list] = 1.0): # Cache grid info self.dim = len(shape) + """int: Number of dimensions.""" + self.shape = shape + """tuple: Shape of grid, using matrix/tensor indexing.""" + self.voxel_size = ( np.array(voxel_size) if isinstance(voxel_size, list) else voxel_size * np.ones(self.dim) ) + """np.ndarray: Size of voxels in each dimension.""" + assert len(self.voxel_size) == self.dim # Define cell and face numbering @@ -40,93 +48,210 @@ def _setup(self) -> None: # Define dimensions of the problem and indexing of cells, from here one start # counting rows from left to right, from top to bottom. - num_cells = np.prod(self.shape) - flat_numbering_cells = np.arange(num_cells, dtype=int) - numbering_cells = flat_numbering_cells.reshape(self.shape) + self.num_cells = np.prod(self.shape) + """int: Number of cells.""" + + # TODO rename -> cell_index + self.numbering_cells = np.arange(self.num_cells, dtype=int).reshape(self.shape) + """np.ndarray: Numbering of cells.""" # Consider only inner faces; implicitly define indexing of faces (first # vertical, then horizontal). The counting of vertical faces starts from top to # bottom and left to right. The counting of horizontal faces starts from left to # right and top to bottom. - vertical_faces_shape = (self.shape[0], self.shape[1] - 1) - horizontal_faces_shape = (self.shape[0] - 1, self.shape[1]) - num_vertical_faces = np.prod(vertical_faces_shape) - num_horizontal_faces = np.prod(horizontal_faces_shape) - num_faces_axis = [ - num_vertical_faces, - num_horizontal_faces, + # vertical_faces_shape = (self.shape[0], self.shape[1] - 1) + # horizontal_faces_shape = (self.shape[0] - 1, self.shape[1]) + # num_vertical_faces = np.prod(vertical_faces_shape) + # num_horizontal_faces = np.prod(horizontal_faces_shape) + # num_faces_axis = [ + # num_vertical_faces, + # num_horizontal_faces, + # ] + # num_faces = np.sum(num_faces_axis) + + # Determine number of inner faces in each axis + self.inner_faces_shape = [ + tuple(np.array(self.shape) - np.eye(self.dim, dtype=int)[d]) + for d in range(self.dim) ] - num_faces = np.sum(num_faces_axis) + """list: Shape of inner faces in each axis.""" - # Define flat indexing of faces: vertical faces first, then horizontal faces - flat_vertical_faces = np.arange(num_vertical_faces, dtype=int) - flat_horizontal_faces = num_vertical_faces + np.arange( - num_horizontal_faces, dtype=int - ) - vertical_faces = flat_vertical_faces.reshape(vertical_faces_shape) - horizontal_faces = flat_horizontal_faces.reshape(horizontal_faces_shape) - - # Identify vertical faces on top, inner and bottom - self.top_row_vertical_faces = np.ravel(vertical_faces[0, :]) - self.inner_vertical_faces = np.ravel(vertical_faces[1:-1, :]) - self.bottom_row_vertical_faces = np.ravel(vertical_faces[-1, :]) - # Identify horizontal faces on left, inner and right - self.left_col_horizontal_faces = np.ravel(horizontal_faces[:, 0]) - self.inner_horizontal_faces = np.ravel(horizontal_faces[:, 1:-1]) - self.right_col_horizontal_faces = np.ravel(horizontal_faces[:, -1]) + self.num_inner_faces = [np.prod(s) for s in self.inner_faces_shape] + """list: Number of inner faces in each axis.""" + + self.num_faces = np.sum(self.num_inner_faces) + """int: Number of faces.""" + + # Define flat indexing of faces, and order of faces, sorted by orientation. + # vertical faces first, then horizontal faces + # flat_vertical_faces = np.arange(num_vertical_faces, dtype=int) + # flat_horizontal_faces = num_vertical_faces + np.arange( + # num_horizontal_faces, dtype=int + # ) + # vertical_faces = flat_vertical_faces.reshape(vertical_faces_shape) + # horizontal_faces = flat_horizontal_faces.reshape(horizontal_faces_shape) + + # Define indexing and ordering of inner faces. Horizontal -> vertical -> depth. + # TODO replace with slices + self.flat_inner_faces = [ + sum(self.num_inner_faces[:d]) + + np.arange(self.num_inner_faces[d], dtype=int) + for d in range(self.dim) + ] + + self.inner_faces = [ + self.flat_inner_faces[d].reshape(self.inner_faces_shape[d]) + for d in range(self.dim) + ] + + # # Identify vertical faces on top, inner and bottom + # self.top_row_vertical_faces = np.ravel(vertical_faces[0, :]) + # self.inner_vertical_faces = np.ravel(vertical_faces[1:-1, :]) + # self.bottom_row_vertical_faces = np.ravel(vertical_faces[-1, :]) + # # Identify horizontal faces on left, inner and right + # self.left_col_horizontal_faces = np.ravel(horizontal_faces[:, 0]) + # self.inner_horizontal_faces = np.ravel(horizontal_faces[:, 1:-1]) + # self.right_col_horizontal_faces = np.ravel(horizontal_faces[:, -1]) + + # Identify inner faces (full cube) + if self.dim == 1: + self.interior_inner_faces = [ + np.ravel(self.inner_faces[0][1:-1]), + ] + elif self.dim == 2: + self.interior_inner_faces = [ + np.ravel(self.inner_faces[0][:, 1:-1]), + np.ravel(self.inner_faces[1][1:-1, :]), + ] + elif self.dim == 3: + self.interior_inner_faces = [ + np.ravel(self.inner_faces[0][:, 1:-1, 1:-1]), + np.ravel(self.inner_faces[1][1:-1, :, 1:-1]), + np.ravel(self.inner_faces[2][1:-1, 1:-1, :]), + ] + else: + raise NotImplementedError(f"Grid of dimension {self.dim} not implemented.") + + # Identify all faces on the outer boundary of the grid. Need to use hardcoded + # knowledge of the orientation of axes and grid indexing. + if self.dim == 1: + self.exterior_inner_faces = [ + np.ravel(self.inner_faces[0][np.array([0, -1])]) + ] + elif self.dim == 2: + self.exterior_inner_faces = [ + np.ravel(self.inner_faces[0][:, np.array([0, -1])]), + np.ravel(self.inner_faces[1][np.array([0, -1]), :]), + ] + elif self.dim == 3: + # TODO + raise NotImplementedError + self.outer_faces = [] + else: + raise NotImplementedError(f"Grid of dimension {self.dim} not implemented.") # ! ---- Connectivity ---- - # Define connectivity and direction of the normal on faces - connectivity = np.zeros((num_faces, 2), dtype=int) - # Vertical faces to left cells - connectivity[: num_faces_axis[0], 0] = np.ravel(numbering_cells[:, :-1]) - # Vertical faces to right cells - connectivity[: num_faces_axis[0], 1] = np.ravel(numbering_cells[:, 1:]) - # Horizontal faces to top cells - connectivity[num_faces_axis[0] :, 0] = np.ravel(numbering_cells[:-1, :]) - # Horizontal faces to bottom cells - connectivity[num_faces_axis[0] :, 1] = np.ravel(numbering_cells[1:, :]) - - # Define reverse connectivity. Cell to vertical faces - self.connectivity_cell_to_vertical_face = -np.ones((num_cells, 2), dtype=int) - # Left vertical face of cell - self.connectivity_cell_to_vertical_face[ - np.ravel(numbering_cells[:, 1:]), 0 - ] = flat_vertical_faces - # Right vertical face of cell - self.connectivity_cell_to_vertical_face[ - np.ravel(numbering_cells[:, :-1]), 1 - ] = flat_vertical_faces - # Define reverse connectivity. Cell to horizontal faces - self.connectivity_cell_to_horizontal_face = np.zeros((num_cells, 2), dtype=int) - # Top horizontal face of cell - self.connectivity_cell_to_horizontal_face[ - np.ravel(numbering_cells[1:, :]), 0 - ] = flat_horizontal_faces - # Bottom horizontal face of cell - self.connectivity_cell_to_horizontal_face[ - np.ravel(numbering_cells[:-1, :]), 1 - ] = flat_horizontal_faces + self.connectivity = np.zeros((self.num_faces, 2), dtype=int) + """np.ndarray: Connectivity (and direction) of faces to cells.""" + if self.dim >= 1: + self.connectivity[self.flat_inner_faces[0], 0] = np.ravel( + self.numbering_cells[:-1, ...] + ) + self.connectivity[self.flat_inner_faces[0], 1] = np.ravel( + self.numbering_cells[1:, ...] + ) + if self.dim >= 2: + self.connectivity[self.flat_inner_faces[1], 0] = np.ravel( + self.numbering_cells[:, :-1, ...] + ) + self.connectivity[self.flat_inner_faces[1], 1] = np.ravel( + self.numbering_cells[:, 1:, ...] + ) + if self.dim >= 3: + self.connectivity[self.flat_inner_faces[2], 0] = np.ravel( + self.numbering_cells[:, :, -1, ...] + ) + self.connectivity[self.flat_inner_faces[2], 1] = np.ravel( + self.numbering_cells[:, :, 1:, ...] + ) + if self.dim > 3: + raise NotImplementedError(f"Grid of dimension {self.dim} not implemented.") + + ## Vertical faces to left cells + # connectivity[: num_faces_axis[0], 0] = np.ravel(numbering_cells[:, :-1]) + ## Vertical faces to right cells + # connectivity[: num_faces_axis[0], 1] = np.ravel(numbering_cells[:, 1:]) + ## Horizontal faces to top cells + # connectivity[num_faces_axis[0] :, 0] = np.ravel(numbering_cells[:-1, :]) + ## Horizontal faces to bottom cells + # connectivity[num_faces_axis[0] :, 1] = np.ravel(numbering_cells[1:, :]) + + self.reverse_connectivity = -np.ones((self.dim, self.num_cells, 2), dtype=int) + """np.ndarray: Reverse connectivity (and direction) of cells to faces.""" + + # NOTE: The first components addresses the cell, the second the axis, the third + # the direction of the relative position of the face wrt the cell (0: left/up, + # 1: right/down, using matrix indexing in 2d - analogously in 3d). + + if self.dim >= 1: + self.reverse_connectivity[ + 0, np.ravel(self.numbering_cells[1:, ...]), 0 + ] = self.flat_inner_faces[0] + self.reverse_connectivity[ + 0, np.ravel(self.numbering_cells[:-1, ...]), 1 + ] = self.flat_inner_faces[0] + + if self.dim >= 2: + self.reverse_connectivity[ + 1, np.ravel(self.numbering_cells[:, 1:, ...]), 0 + ] = self.flat_inner_faces[1] + self.reverse_connectivity[ + 1, np.ravel(self.numbering_cells[:, :-1, ...]), 1 + ] = self.flat_inner_faces[1] + + if self.dim >= 3: + self.reverse_connectivity[ + 2, np.ravel(self.numbering_cells[:, :, 1:, ...]), 0 + ] = self.flat_inner_faces[2] + self.reverse_connectivity[ + 2, np.ravel(self.numbering_cells[:, :, :-1, ...]), 1 + ] = self.flat_inner_faces[2] + + ## Define reverse connectivity. Cell to vertical faces + # self.connectivity_cell_to_vertical_face = -np.ones((self.num_cells, 2), dtype=int) + ## Left vertical face of cell + # self.connectivity_cell_to_vertical_face[ + # np.ravel(numbering_cells[:, 1:]), 0 + # ] = flat_vertical_faces + ## Right vertical face of cell + # self.connectivity_cell_to_vertical_face[ + # np.ravel(numbering_cells[:, :-1]), 1 + # ] = flat_vertical_faces + ## Define reverse connectivity. Cell to horizontal faces + # self.connectivity_cell_to_horizontal_face = np.zeros((self.num_cells, 2), dtype=int) + ## Top horizontal face of cell + # self.connectivity_cell_to_horizontal_face[ + # np.ravel(numbering_cells[1:, :]), 0 + # ] = flat_horizontal_faces + ## Bottom horizontal face of cell + # self.connectivity_cell_to_horizontal_face[ + # np.ravel(numbering_cells[:-1, :]), 1 + # ] = flat_horizontal_faces # Info about inner cells # TODO rm? - self.inner_cells_with_vertical_faces = np.ravel(numbering_cells[:, 1:-1]) - self.inner_cells_with_horizontal_faces = np.ravel(numbering_cells[1:-1, :]) - - # ! ---- Cache ---- - # TODO reduce - self.num_faces = num_faces - self.num_cells = num_cells - self.num_vertical_faces = num_vertical_faces - self.num_horizontal_faces = num_horizontal_faces - self.numbering_cells = numbering_cells - self.num_faces_axis = num_faces_axis - self.vertical_faces_shape = vertical_faces_shape - self.horizontal_faces_shape = horizontal_faces_shape - self.connectivity = connectivity - self.flat_vertical_faces = flat_vertical_faces - self.flat_horizontal_faces = flat_horizontal_faces + # self.inner_cells_with_vertical_faces = np.ravel(numbering_cells[:, 1:-1]) + # self.inner_cells_with_horizontal_faces = np.ravel(numbering_cells[1:-1, :]) + self.inner_cells_with_inner_faces = ( + [] + [np.ravel(self.numbering_cells[1:-1, ...])] + if self.dim >= 1 + else [] + [np.ravel(self.numbering_cells[:, 1:-1, ...])] + if self.dim >= 2 + else [] + [np.ravel(self.numbering_cells[:, :, 1:-1, ...])] + if self.dim >= 3 + else [] + ) def generate_grid(image: darsia.Image) -> Grid: From ca9764b8f4e8c53e8347e1ec881105519e205c9a Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Tue, 19 Sep 2023 12:16:00 +0200 Subject: [PATCH 072/100] TST: Provide test capabilities for 2d grids. --- tests/unit/test_grid.py | 75 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 tests/unit/test_grid.py diff --git a/tests/unit/test_grid.py b/tests/unit/test_grid.py new file mode 100644 index 00000000..d0ef60b6 --- /dev/null +++ b/tests/unit/test_grid.py @@ -0,0 +1,75 @@ +"""Unit tests for the grid module.""" + +import numpy as np + +import darsia + + +def test_grid_2d(): + grid = darsia.Grid(shape=(4, 5)) + + # Check basic attributes + assert np.allclose(grid.shape, (4, 5)) + assert grid.dim == 2 + assert np.allclose(grid.voxel_size, (1, 1)) + + # Probe cell numbering + assert grid.num_cells == 20 + assert grid.numbering_cells[0, 0] == 0 + assert grid.numbering_cells[0, 4] == 4 + assert grid.numbering_cells[3, 0] == 15 + assert grid.numbering_cells[3, 4] == 19 + + # Check face shape + assert np.allclose(grid.inner_faces_shape[0], (3, 5)) + assert np.allclose(grid.inner_faces_shape[1], (4, 4)) + + # Check face numbering + assert grid.num_inner_faces[0] == 15 + assert grid.num_inner_faces[1] == 16 + assert grid.num_faces == 15 + 16 + + # Check indexing of faces in flat format + assert np.allclose(grid.flat_inner_faces[0], np.arange(0, 15)) + assert np.allclose(grid.flat_inner_faces[1], np.arange(15, 31)) + + # Check indexing of faces in 2d format + assert np.allclose(grid.inner_faces[0][0], np.arange(0, 5)) + assert np.allclose(grid.inner_faces[0][1], np.arange(5, 10)) + assert np.allclose(grid.inner_faces[0][2], np.arange(10, 15)) + assert np.allclose(grid.inner_faces[1][0], np.arange(15, 19)) + assert np.allclose(grid.inner_faces[1][1], np.arange(19, 23)) + assert np.allclose(grid.inner_faces[1][2], np.arange(23, 27)) + + # Check identification of interior inner faces + assert np.allclose(grid.interior_inner_faces[0], [1, 2, 3, 6, 7, 8, 11, 12, 13]) + assert np.allclose(grid.interior_inner_faces[1], [19, 20, 21, 22, 23, 24, 25, 26]) + + # Check identification of exterior inner faces + assert np.allclose(grid.exterior_inner_faces[0], [0, 4, 5, 9, 10, 14]) + assert np.allclose(grid.exterior_inner_faces[1], [15, 16, 17, 18, 27, 28, 29, 30]) + + # Check connectivity: face to cells with positive orientation + assert np.allclose(grid.connectivity[0], [0, 5]) + assert np.allclose(grid.connectivity[4], [4, 9]) + assert np.allclose(grid.connectivity[10], [10, 15]) + assert np.allclose(grid.connectivity[14], [14, 19]) + assert np.allclose(grid.connectivity[15], [0, 1]) + assert np.allclose(grid.connectivity[18], [3, 4]) + assert np.allclose(grid.connectivity[27], [15, 16]) + assert np.allclose(grid.connectivity[30], [18, 19]) + + # Check reverse connectivity: cell to faces with positive orientation + # For corner cells + assert np.allclose(grid.reverse_connectivity[0, 0], [-1, 0]) + assert np.allclose(grid.reverse_connectivity[1, 0], [-1, 15]) + assert np.allclose(grid.reverse_connectivity[0, 4], [-1, 4]) + assert np.allclose(grid.reverse_connectivity[1, 4], [18, -1]) + assert np.allclose(grid.reverse_connectivity[0, 15], [10, -1]) + assert np.allclose(grid.reverse_connectivity[1, 15], [-1, 27]) + assert np.allclose(grid.reverse_connectivity[0, 19], [14, -1]) + assert np.allclose(grid.reverse_connectivity[1, 19], [30, -1]) + + # For interior cells + assert np.allclose(grid.reverse_connectivity[0, 6], [1, 6]) + assert np.allclose(grid.reverse_connectivity[1, 6], [19, 20]) From 255a6127699f7cedbf171275106546509cebf758 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Tue, 19 Sep 2023 12:48:19 +0200 Subject: [PATCH 073/100] BUG: Fix divergence - wrong sorting. --- src/darsia/utils/fv.py | 30 +++++++++--------------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/src/darsia/utils/fv.py b/src/darsia/utils/fv.py index fce504dc..d739c0a4 100644 --- a/src/darsia/utils/fv.py +++ b/src/darsia/utils/fv.py @@ -20,30 +20,18 @@ def __init__(self, grid: darsia.Grid) -> None: # faces. div_shape = (grid.num_cells, grid.num_faces) div_data = np.concatenate( - ( - grid.voxel_size[0] * np.ones(grid.num_vertical_faces, dtype=float), - grid.voxel_size[1] * np.ones(grid.num_horizontal_faces, dtype=float), - -grid.voxel_size[0] * np.ones(grid.num_vertical_faces, dtype=float), - -grid.voxel_size[1] * np.ones(grid.num_horizontal_faces, dtype=float), - ) + [ + grid.face_vol[d] * np.tile([1, -1], grid.num_inner_faces[d]) + for d in range(grid.dim) + ] ) div_row = np.concatenate( - ( - grid.connectivity[ - grid.flat_vertical_faces, 0 - ], # vertical faces, cells to the left - grid.connectivity[ - grid.flat_horizontal_faces, 0 - ], # horizontal faces, cells to the top - grid.connectivity[ - grid.flat_vertical_faces, 1 - ], # vertical faces, cells to the right (opposite normal) - grid.connectivity[ - grid.flat_horizontal_faces, 1 - ], # horizontal faces, cells to the bottom (opposite normal) - ) + [ + np.ravel(grid.connectivity[grid.flat_inner_faces[d]]) + for d in range(grid.dim) + ] ) - div_col = np.tile(np.arange(grid.num_faces, dtype=int), 2) + div_col = np.repeat(np.arange(grid.num_faces, dtype=int), 2) div = sps.csc_matrix( (div_data, (div_row, div_col)), shape=div_shape, From 77fe1b92d5fbd0ae205959aba2c63bb833028d21 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Tue, 19 Sep 2023 12:48:39 +0200 Subject: [PATCH 074/100] ENH: Provide face volumes for grids. --- src/darsia/utils/grid.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/darsia/utils/grid.py b/src/darsia/utils/grid.py index e6d61bba..0aad545a 100644 --- a/src/darsia/utils/grid.py +++ b/src/darsia/utils/grid.py @@ -36,6 +36,12 @@ def __init__(self, shape: tuple, voxel_size: Union[float, list] = 1.0): ) """np.ndarray: Size of voxels in each dimension.""" + self.face_vol = [ + np.prod(self.voxel_size[np.delete(np.arange(self.dim), d)]) + for d in range(self.dim) + ] + """list: Volume of faces in each dimension.""" + assert len(self.voxel_size) == self.dim # Define cell and face numbering From 4aaee279dcf7c8a68c4155c3e0b703e5f3b9576e Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Tue, 19 Sep 2023 12:51:23 +0200 Subject: [PATCH 075/100] TST: Enhance grid test, check nontrivial voxel size --- tests/unit/test_grid.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_grid.py b/tests/unit/test_grid.py index d0ef60b6..54cd2468 100644 --- a/tests/unit/test_grid.py +++ b/tests/unit/test_grid.py @@ -6,12 +6,12 @@ def test_grid_2d(): - grid = darsia.Grid(shape=(4, 5)) + grid = darsia.Grid(shape=(4, 5), voxel_size=[0.5, 0.25]) # Check basic attributes assert np.allclose(grid.shape, (4, 5)) assert grid.dim == 2 - assert np.allclose(grid.voxel_size, (1, 1)) + assert np.allclose(grid.voxel_size, [0.5, 0.25]) # Probe cell numbering assert grid.num_cells == 20 @@ -20,6 +20,9 @@ def test_grid_2d(): assert grid.numbering_cells[3, 0] == 15 assert grid.numbering_cells[3, 4] == 19 + # Check face volumes + assert np.allclose(grid.face_vol, [0.25, 0.5]) + # Check face shape assert np.allclose(grid.inner_faces_shape[0], (3, 5)) assert np.allclose(grid.inner_faces_shape[1], (4, 4)) From 2b962a6f488fddd247ba7c8b88b0ebffe8a3e82b Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Tue, 19 Sep 2023 12:52:02 +0200 Subject: [PATCH 076/100] TST: Provide test for fv divergence operator. --- tests/unit/test_fv.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 tests/unit/test_fv.py diff --git a/tests/unit/test_fv.py b/tests/unit/test_fv.py new file mode 100644 index 00000000..9fe8ea97 --- /dev/null +++ b/tests/unit/test_fv.py @@ -0,0 +1,33 @@ +"""Unit tests for finite volume utilities.""" + +import numpy as np + +import darsia + + +def test_divergence_2d(): + # Create divergence matrix + grid = darsia.Grid(shape=(4, 5), voxel_size=[0.5, 0.25]) + divergence = darsia.FVDivergence(grid).mat.todense() + + # Check shape + assert np.allclose(divergence.shape, (grid.num_cells, grid.num_faces)) + + # Check values in corner cells + assert np.allclose(np.nonzero(divergence[0])[1], [0, 15]) + assert np.allclose(divergence[0, np.array([0, 15])], [0.25, 0.5]) + + assert np.allclose(np.nonzero(divergence[4])[1], [4, 18]) + assert np.allclose(divergence[4, np.array([4, 18])], [0.25, -0.5]) + + assert np.allclose(np.nonzero(divergence[15])[1], [10, 27]) + assert np.allclose(divergence[15, np.array([10, 27])], [-0.25, 0.5]) + + assert np.allclose(np.nonzero(divergence[19])[1], [14, 30]) + assert np.allclose(divergence[19, np.array([14, 30])], [-0.25, -0.5]) + + # Check value for interior cell + assert np.allclose(np.nonzero(divergence[6])[1], [1, 6, 19, 20]) + assert np.allclose( + divergence[6, np.array([1, 6, 19, 20])], [-0.25, 0.25, -0.5, 0.5] + ) From 85b415f2c70c09a5469c7192517f05a7812264df Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Tue, 19 Sep 2023 13:01:04 +0200 Subject: [PATCH 077/100] DOC: Add better descriptions of FV ojects. --- src/darsia/utils/fv.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/darsia/utils/fv.py b/src/darsia/utils/fv.py index d739c0a4..349b82a9 100644 --- a/src/darsia/utils/fv.py +++ b/src/darsia/utils/fv.py @@ -14,10 +14,10 @@ class FVDivergence: def __init__(self, grid: darsia.Grid) -> None: # Define sparse divergence operator, integrated over elements. # Note: The global direction of the degrees of freedom is hereby fixed for all - # faces. Fluxes across vertical faces go from left to right, fluxes across - # horizontal faces go from bottom to top. To oppose the direction of the outer - # normal, the sign of the divergence is flipped for one side of cells for all - # faces. + # faces. In 2d, fluxes across vertical faces go from left to right, fluxes + # across horizontal faces go from bottom to top. To oppose the direction of the + # outer normal, the sign of the divergence is flipped for one side of cells for + # all faces. Analogously, in 3d. div_shape = (grid.num_cells, grid.num_faces) div_data = np.concatenate( [ @@ -42,6 +42,17 @@ def __init__(self, grid: darsia.Grid) -> None: class FVMass: + """Finite volume mass matrix. + + The mass matrix can be formulated for cell and face quantities. For cell + quantities, the mass matrix is diagonal and has the volume of the cells on the + diagonal. For face quantities, the mass matrix is diagonal and has half the cell + volumes on the diagonal, taking into accout itegration over te faces as lower + dimensional entities, and the double occurrence of the faces in the integration + over the cells. + + """ + def __init__( self, grid: darsia.Grid, mode: str = "cells", lumping: bool = True ) -> None: @@ -56,6 +67,8 @@ def __init__( np.prod(grid.voxel_size) * np.ones(grid.num_faces, dtype=float) ) else: + raise NotImplementedError + # Define true RT0 mass matrix on faces: flat fluxes -> flat fluxes num_inner_cells_with_vertical_faces = len( grid.inner_cells_with_vertical_faces @@ -307,6 +320,8 @@ def cell_to_face(grid: darsia.Grid, cell_qty: np.ndarray, mode: str) -> np.ndarr # NOTE: No impact of Grid here, so far! Everything is implicit. This should/could # change. In particular when switching to 3d! + raise NotImplementedError + # Determine the fluxes on the faces if mode == "arithmetic": # Employ arithmetic averaging From 5c9ab254ae29176b4594e264f013a6941c3d84b3 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Tue, 19 Sep 2023 13:01:37 +0200 Subject: [PATCH 078/100] MAINT: Generalize/adapt reconstruction of tangential fluxes on faces. --- src/darsia/utils/fv.py | 191 ++++++++++++++++++++--------------------- 1 file changed, 94 insertions(+), 97 deletions(-) diff --git a/src/darsia/utils/fv.py b/src/darsia/utils/fv.py index 349b82a9..67c86361 100644 --- a/src/darsia/utils/fv.py +++ b/src/darsia/utils/fv.py @@ -150,112 +150,109 @@ def __init__( self.mat = mass_matrix -class FVFaceAverage: +class FVTangentialReconstruction: + """Projection of normal fluxes on grid onto tangential components. + + The tangential components are defined as the components of the fluxes that are + orthogonal to the normal of the face. The tangential components are determined + through averaging the fluxes on the faces that are orthogonal to the face of + interest. + + The resulting tangential flux has co-dimension 1, i.e. it is a scalar quantity in + 2d and a 2-valued vector quantity in 3d. + + """ + def __init__(self, grid: darsia.Grid) -> None: + """Initialize the average operator. + + Args: + grid (darsia.Grid): grid + + """ + # Operator for averaging fluxes on orthogonal, neighboring faces - orthogonal_face_average_shape = (grid.num_faces, grid.num_faces) - orthogonal_face_average_data = 0.25 * np.concatenate( - ( - np.ones( - 2 * len(grid.top_row_vertical_faces) - + 4 * len(grid.inner_vertical_faces) - + 2 * len(grid.bottom_row_vertical_faces) - + 2 * len(grid.left_col_horizontal_faces) - + 4 * len(grid.inner_horizontal_faces) - + 2 * len(grid.right_col_horizontal_faces), - dtype=float, - ), - ) + shape = ((grid.dim - 1) * grid.num_faces, grid.num_faces) + + # Each interior inner face has four neighboring faces with normal direction + # and all oriented in the same direction. In three dimensions, two such normal + # directions exist. For outer inner faces, there are only two such neighboring + # faces. + data = np.tile( + 0.25 + * np.ones( + 2 * np.array(grid.exterior_inner_faces).size + + 4 * np.array(grid.interior_inner_faces).size, + dtype=float, + ), + grid.dim - 1, ) - orthogonal_face_average_rows = np.concatenate( - ( - np.tile(grid.top_row_vertical_faces, 2), - np.tile(grid.inner_vertical_faces, 4), - np.tile(grid.bottom_row_vertical_faces, 2), - np.tile(grid.left_col_horizontal_faces, 2), - np.tile(grid.inner_horizontal_faces, 4), - np.tile(grid.right_col_horizontal_faces, 2), - ) + + # The rows correspond to the faces for which the tangential fluxes are + # determined times the component of the tangential fluxes. + rows_outer = np.concatenate( + [np.repeat(grid.exterior_inner_faces[d], 2) for d in range(grid.dim)] ) - orthogonal_face_average_cols = np.concatenate( - ( - # top row: left cell -> bottom face - grid.connectivity_cell_to_horizontal_face[ - grid.connectivity[grid.top_row_vertical_faces, 0], 1 - ], - # top row: vertical face -> right cell -> bottom face - grid.connectivity_cell_to_horizontal_face[ - grid.connectivity[grid.top_row_vertical_faces, 1], 1 - ], - # inner rows: vertical face -> left cell -> top face - grid.connectivity_cell_to_horizontal_face[ - grid.connectivity[grid.inner_vertical_faces, 0], 0 - ], - # inner rows: vertical face -> left cell -> bottom face - grid.connectivity_cell_to_horizontal_face[ - grid.connectivity[grid.inner_vertical_faces, 0], 1 - ], - # inner rows: vertical face -> right cell -> top face - grid.connectivity_cell_to_horizontal_face[ - grid.connectivity[grid.inner_vertical_faces, 1], 0 - ], - # inner rows: vertical face -> right cell -> bottom face - grid.connectivity_cell_to_horizontal_face[ - grid.connectivity[grid.inner_vertical_faces, 1], 1 - ], - # bottom row: vertical face -> left cell -> top face - grid.connectivity_cell_to_horizontal_face[ - grid.connectivity[grid.bottom_row_vertical_faces, 0], 0 - ], - # bottom row: vertical face -> right cell -> top face - grid.connectivity_cell_to_horizontal_face[ - grid.connectivity[grid.bottom_row_vertical_faces, 1], 0 - ], - # left column: horizontal face -> top cell -> right face - grid.connectivity_cell_to_vertical_face[ - grid.connectivity[grid.left_col_horizontal_faces, 0], 1 - ], - # left column: horizontal face -> bottom cell -> right face - grid.connectivity_cell_to_vertical_face[ - grid.connectivity[grid.left_col_horizontal_faces, 1], 1 - ], - # inner columns: horizontal face -> top cell -> left face - grid.connectivity_cell_to_vertical_face[ - grid.connectivity[grid.inner_horizontal_faces, 0], 0 - ], - # inner columns: horizontal face -> top cell -> right face - grid.connectivity_cell_to_vertical_face[ - grid.connectivity[grid.inner_horizontal_faces, 0], 1 - ], - # inner columns: horizontal face -> bottom cell -> left face - grid.connectivity_cell_to_vertical_face[ - grid.connectivity[grid.inner_horizontal_faces, 1], 0 - ], - # inner columns: horizontal face -> bottom cell -> right face - grid.connectivity_cell_to_vertical_face[ - grid.connectivity[grid.inner_horizontal_faces, 1], 1 - ], - # right column: horizontal face -> top cell -> left face - grid.connectivity_cell_to_vertical_face[ - grid.connectivity[grid.right_col_horizontal_faces, 0], 0 - ], - # right column: horizontal face -> bottom cell -> left face - grid.connectivity_cell_to_vertical_face[ - grid.connectivity[grid.right_col_horizontal_faces, 1], 0 - ], - ) + + rows_inner = np.concatenate( + [np.repeat(grid.interior_inner_faces[d], 4) for d in range(grid.dim)] + ) + + # The columns correspond to the (orthogonal) faces contributing to the average + # of the tangential fluxes. The main idea is for each face to follow the + # connectivity. First, we consider the outer inner faces. For each face, we + + # Consider outer inner faces. For each face, we consider the two neighboring + # faces with normal direction and all oriented in the same direction. Need to + # exclude true exterior faces. + + def interleaf(a: np.ndarray, b: np.ndarray) -> np.ndarray: + return np.ravel(np.vstack([a, b]).T) + + # Consider all close-by faces for each outer inner face which are orthogonal + pre_cols_outer = np.concatenate( + [ + np.ravel( + grid.reverse_connectivity[ + d_perp, + np.ravel(grid.connectivity[grid.exterior_inner_faces[d]]), + ] + ) + for d in range(grid.dim) + for d_perp in np.delete(range(grid.dim), d) + ] + ) + # Clean up - remove true exterior faces + pre_cols_outer = pre_cols_outer[pre_cols_outer != -1] + + # Same for interior inner faces, + pre_cols_inner = np.concatenate( + [ + np.ravel( + grid.reverse_connectivity[ + d_perp, + np.ravel(grid.connectivity[grid.interior_inner_faces[d]]), + ] + ) + for d in range(grid.dim) + for d_perp in np.delete(range(grid.dim), d) + ] ) - orthogonal_face_average = sps.csc_matrix( + assert np.count_nonzero(pre_cols_inner == -1) == 0 + + # Collect all rows and columns + rows = np.concatenate((rows_outer, rows_inner)) + cols = np.concatenate((pre_cols_outer, pre_cols_inner)) + + # Construct and cache the sparse projection matrix + self.mat = sps.csc_matrix( ( - orthogonal_face_average_data, - (orthogonal_face_average_rows, orthogonal_face_average_cols), + data, + (rows, cols), ), - shape=orthogonal_face_average_shape, + shape=shape, ) - # Cache - self.mat = orthogonal_face_average - # ! ---- Finite volume projection operators ---- From 425875f9b7a0a0fdbe3d55ff9b4c76216c5a9852 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Tue, 19 Sep 2023 13:01:59 +0200 Subject: [PATCH 079/100] MAINT: Generalize/adapt vector reconstruction of fluxes on cells. --- src/darsia/utils/fv.py | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/src/darsia/utils/fv.py b/src/darsia/utils/fv.py index 67c86361..08a7a185 100644 --- a/src/darsia/utils/fv.py +++ b/src/darsia/utils/fv.py @@ -275,23 +275,32 @@ def face_to_cell(grid: darsia.Grid, flat_flux: np.ndarray) -> np.ndarray: np.ndarray: cell-based vectorial fluxes """ - # Reshape fluxes - use duality of faces and normals - horizontal_fluxes = flat_flux[: grid.num_faces_axis[0]].reshape( - grid.vertical_faces_shape - ) - vertical_fluxes = flat_flux[grid.num_faces_axis[0] :].reshape( - grid.horizontal_faces_shape - ) - - # Determine a cell-based Raviart-Thomas reconstruction of the fluxes, projected - # onto piecewise constant functions. + # TODO revert order of indices cell_flux = np.zeros((*grid.shape, grid.dim), dtype=float) - # Horizontal fluxes - cell_flux[:, :-1, 0] += 0.5 * horizontal_fluxes - cell_flux[:, 1:, 0] += 0.5 * horizontal_fluxes - # Vertical fluxes - cell_flux[:-1, :, 1] += 0.5 * vertical_fluxes - cell_flux[1:, :, 1] += 0.5 * vertical_fluxes + + if grid.dim >= 1: + cell_flux[:-1, ..., 0] += 0.5 * flat_flux[grid.flat_inner_faces[0]].reshape( + grid.inner_faces_shape[0] + ) + cell_flux[1:, ..., 0] += 0.5 * flat_flux[grid.flat_inner_faces[0]].reshape( + grid.inner_faces_shape[0] + ) + if grid.dim >= 2: + cell_flux[:, :-1, ..., 1] += 0.5 * flat_flux[grid.flat_inner_faces[1]].reshape( + grid.inner_faces_shape[1] + ) + cell_flux[:, 1:, ..., 1] += 0.5 * flat_flux[grid.flat_inner_faces[1]].reshape( + grid.inner_faces_shape[1] + ) + if grid.dim >= 3: + cell_flux[:, :, :-1, ..., 2] += 0.5 * flat_flux[ + grid.flat_inner_faces[2] + ].reshape(grid.inner_faces_shape[2]) + cell_flux[:, :, 1:, ..., 2] += 0.5 * flat_flux[ + grid.flat_inner_faces[2] + ].reshape(grid.inner_faces_shape[2]) + if grid.dim > 3: + raise NotImplementedError(f"Dimension {grid.dim} not supported.") return cell_flux From bbc163f9fee942a4f5172d24c048569a92a75b56 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Tue, 19 Sep 2023 13:02:29 +0200 Subject: [PATCH 080/100] MAINT: Use updated tangential reconstruction in Wasserstein computations. --- src/darsia/measure/wasserstein.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index f227c369..7905c4d1 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -173,8 +173,8 @@ def _setup_discretization(self) -> None: self.mass_matrix_faces = darsia.FVMass(self.grid, "faces", lumping).mat """sps.csc_matrix: mass matrix on faces: flat fluxes -> flat fluxes""" - self.orthogonal_face_average = darsia.FVFaceAverage(self.grid).mat - """sps.csc_matrix: averaging operator for fluxes on orthogonal faces""" + self.tangential_projection = darsia.FVTangentialReconstruction(self.grid).mat + """sps.csc_matrix: tangential reconstruction: flat fluxes -> flat fluxes""" # Linear part of the Darcy operator with potential constraint. self.broken_darcy = sps.bmat( @@ -470,7 +470,7 @@ def vector_face_flux_norm(self, flat_flux: np.ndarray, mode: str) -> np.ndarray: elif mode == "face_arithmetic": # Define natural vector valued flux on faces (taking arithmetic averages # of continuous fluxes over cells evaluated at faces) - tangential_flux = self.orthogonal_face_average.dot(flat_flux) + tangential_flux = self.tangential_projection.dot(flat_flux) # Determine the l2 norm of the fluxes on the faces, add some regularization flat_flux_norm = np.sqrt(flat_flux**2 + tangential_flux**2) @@ -1194,7 +1194,7 @@ def _shrink( elif mode == "face_arithmetic": # Define natural vector valued flux on faces (taking arithmetic averages # of continuous fluxes over cells evaluated at faces) - tangential_flux = self.orthogonal_face_average.dot(flat_flux) + tangential_flux = self.tangential_projection.dot(flat_flux) # Determine the l2 norm of the fluxes on the faces, add some regularization norm = np.sqrt(flat_flux**2 + tangential_flux**2) flat_scaling = np.maximum(norm - shrink_factor, 0) / ( From 753899e8487c4a0240d28485684931d16fe19fea Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Tue, 19 Sep 2023 13:07:47 +0200 Subject: [PATCH 081/100] MAINT: Clean up file, rename cell index attribute --- src/darsia/utils/grid.py | 99 +++++++--------------------------------- tests/unit/test_grid.py | 8 ++-- 2 files changed, 21 insertions(+), 86 deletions(-) diff --git a/src/darsia/utils/grid.py b/src/darsia/utils/grid.py index 0aad545a..a7abd4d9 100644 --- a/src/darsia/utils/grid.py +++ b/src/darsia/utils/grid.py @@ -57,23 +57,8 @@ def _setup(self) -> None: self.num_cells = np.prod(self.shape) """int: Number of cells.""" - # TODO rename -> cell_index - self.numbering_cells = np.arange(self.num_cells, dtype=int).reshape(self.shape) - """np.ndarray: Numbering of cells.""" - - # Consider only inner faces; implicitly define indexing of faces (first - # vertical, then horizontal). The counting of vertical faces starts from top to - # bottom and left to right. The counting of horizontal faces starts from left to - # right and top to bottom. - # vertical_faces_shape = (self.shape[0], self.shape[1] - 1) - # horizontal_faces_shape = (self.shape[0] - 1, self.shape[1]) - # num_vertical_faces = np.prod(vertical_faces_shape) - # num_horizontal_faces = np.prod(horizontal_faces_shape) - # num_faces_axis = [ - # num_vertical_faces, - # num_horizontal_faces, - # ] - # num_faces = np.sum(num_faces_axis) + self.cell_index = np.arange(self.num_cells, dtype=int).reshape(self.shape) + """np.ndarray: cell indices.""" # Determine number of inner faces in each axis self.inner_faces_shape = [ @@ -88,15 +73,6 @@ def _setup(self) -> None: self.num_faces = np.sum(self.num_inner_faces) """int: Number of faces.""" - # Define flat indexing of faces, and order of faces, sorted by orientation. - # vertical faces first, then horizontal faces - # flat_vertical_faces = np.arange(num_vertical_faces, dtype=int) - # flat_horizontal_faces = num_vertical_faces + np.arange( - # num_horizontal_faces, dtype=int - # ) - # vertical_faces = flat_vertical_faces.reshape(vertical_faces_shape) - # horizontal_faces = flat_horizontal_faces.reshape(horizontal_faces_shape) - # Define indexing and ordering of inner faces. Horizontal -> vertical -> depth. # TODO replace with slices self.flat_inner_faces = [ @@ -110,15 +86,6 @@ def _setup(self) -> None: for d in range(self.dim) ] - # # Identify vertical faces on top, inner and bottom - # self.top_row_vertical_faces = np.ravel(vertical_faces[0, :]) - # self.inner_vertical_faces = np.ravel(vertical_faces[1:-1, :]) - # self.bottom_row_vertical_faces = np.ravel(vertical_faces[-1, :]) - # # Identify horizontal faces on left, inner and right - # self.left_col_horizontal_faces = np.ravel(horizontal_faces[:, 0]) - # self.inner_horizontal_faces = np.ravel(horizontal_faces[:, 1:-1]) - # self.right_col_horizontal_faces = np.ravel(horizontal_faces[:, -1]) - # Identify inner faces (full cube) if self.dim == 1: self.interior_inner_faces = [ @@ -162,37 +129,28 @@ def _setup(self) -> None: """np.ndarray: Connectivity (and direction) of faces to cells.""" if self.dim >= 1: self.connectivity[self.flat_inner_faces[0], 0] = np.ravel( - self.numbering_cells[:-1, ...] + self.cell_index[:-1, ...] ) self.connectivity[self.flat_inner_faces[0], 1] = np.ravel( - self.numbering_cells[1:, ...] + self.cell_index[1:, ...] ) if self.dim >= 2: self.connectivity[self.flat_inner_faces[1], 0] = np.ravel( - self.numbering_cells[:, :-1, ...] + self.cell_index[:, :-1, ...] ) self.connectivity[self.flat_inner_faces[1], 1] = np.ravel( - self.numbering_cells[:, 1:, ...] + self.cell_index[:, 1:, ...] ) if self.dim >= 3: self.connectivity[self.flat_inner_faces[2], 0] = np.ravel( - self.numbering_cells[:, :, -1, ...] + self.cell_index[:, :, -1, ...] ) self.connectivity[self.flat_inner_faces[2], 1] = np.ravel( - self.numbering_cells[:, :, 1:, ...] + self.cell_index[:, :, 1:, ...] ) if self.dim > 3: raise NotImplementedError(f"Grid of dimension {self.dim} not implemented.") - ## Vertical faces to left cells - # connectivity[: num_faces_axis[0], 0] = np.ravel(numbering_cells[:, :-1]) - ## Vertical faces to right cells - # connectivity[: num_faces_axis[0], 1] = np.ravel(numbering_cells[:, 1:]) - ## Horizontal faces to top cells - # connectivity[num_faces_axis[0] :, 0] = np.ravel(numbering_cells[:-1, :]) - ## Horizontal faces to bottom cells - # connectivity[num_faces_axis[0] :, 1] = np.ravel(numbering_cells[1:, :]) - self.reverse_connectivity = -np.ones((self.dim, self.num_cells, 2), dtype=int) """np.ndarray: Reverse connectivity (and direction) of cells to faces.""" @@ -202,59 +160,36 @@ def _setup(self) -> None: if self.dim >= 1: self.reverse_connectivity[ - 0, np.ravel(self.numbering_cells[1:, ...]), 0 + 0, np.ravel(self.cell_index[1:, ...]), 0 ] = self.flat_inner_faces[0] self.reverse_connectivity[ - 0, np.ravel(self.numbering_cells[:-1, ...]), 1 + 0, np.ravel(self.cell_index[:-1, ...]), 1 ] = self.flat_inner_faces[0] if self.dim >= 2: self.reverse_connectivity[ - 1, np.ravel(self.numbering_cells[:, 1:, ...]), 0 + 1, np.ravel(self.cell_index[:, 1:, ...]), 0 ] = self.flat_inner_faces[1] self.reverse_connectivity[ - 1, np.ravel(self.numbering_cells[:, :-1, ...]), 1 + 1, np.ravel(self.cell_index[:, :-1, ...]), 1 ] = self.flat_inner_faces[1] if self.dim >= 3: self.reverse_connectivity[ - 2, np.ravel(self.numbering_cells[:, :, 1:, ...]), 0 + 2, np.ravel(self.cell_index[:, :, 1:, ...]), 0 ] = self.flat_inner_faces[2] self.reverse_connectivity[ - 2, np.ravel(self.numbering_cells[:, :, :-1, ...]), 1 + 2, np.ravel(self.cell_index[:, :, :-1, ...]), 1 ] = self.flat_inner_faces[2] - ## Define reverse connectivity. Cell to vertical faces - # self.connectivity_cell_to_vertical_face = -np.ones((self.num_cells, 2), dtype=int) - ## Left vertical face of cell - # self.connectivity_cell_to_vertical_face[ - # np.ravel(numbering_cells[:, 1:]), 0 - # ] = flat_vertical_faces - ## Right vertical face of cell - # self.connectivity_cell_to_vertical_face[ - # np.ravel(numbering_cells[:, :-1]), 1 - # ] = flat_vertical_faces - ## Define reverse connectivity. Cell to horizontal faces - # self.connectivity_cell_to_horizontal_face = np.zeros((self.num_cells, 2), dtype=int) - ## Top horizontal face of cell - # self.connectivity_cell_to_horizontal_face[ - # np.ravel(numbering_cells[1:, :]), 0 - # ] = flat_horizontal_faces - ## Bottom horizontal face of cell - # self.connectivity_cell_to_horizontal_face[ - # np.ravel(numbering_cells[:-1, :]), 1 - # ] = flat_horizontal_faces - # Info about inner cells # TODO rm? - # self.inner_cells_with_vertical_faces = np.ravel(numbering_cells[:, 1:-1]) - # self.inner_cells_with_horizontal_faces = np.ravel(numbering_cells[1:-1, :]) self.inner_cells_with_inner_faces = ( - [] + [np.ravel(self.numbering_cells[1:-1, ...])] + [] + [np.ravel(self.cell_index[1:-1, ...])] if self.dim >= 1 - else [] + [np.ravel(self.numbering_cells[:, 1:-1, ...])] + else [] + [np.ravel(self.cell_index[:, 1:-1, ...])] if self.dim >= 2 - else [] + [np.ravel(self.numbering_cells[:, :, 1:-1, ...])] + else [] + [np.ravel(self.cell_index[:, :, 1:-1, ...])] if self.dim >= 3 else [] ) diff --git a/tests/unit/test_grid.py b/tests/unit/test_grid.py index 54cd2468..3d0c5efd 100644 --- a/tests/unit/test_grid.py +++ b/tests/unit/test_grid.py @@ -15,10 +15,10 @@ def test_grid_2d(): # Probe cell numbering assert grid.num_cells == 20 - assert grid.numbering_cells[0, 0] == 0 - assert grid.numbering_cells[0, 4] == 4 - assert grid.numbering_cells[3, 0] == 15 - assert grid.numbering_cells[3, 4] == 19 + assert grid.cell_index[0, 0] == 0 + assert grid.cell_index[0, 4] == 4 + assert grid.cell_index[3, 0] == 15 + assert grid.cell_index[3, 4] == 19 # Check face volumes assert np.allclose(grid.face_vol, [0.25, 0.5]) From 96478b3dd8d1c77b2e31f6c35653d13045643bbc Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Tue, 19 Sep 2023 13:23:48 +0200 Subject: [PATCH 082/100] MAINT: Use shorter names for attributes in Grid. --- src/darsia/utils/grid.py | 87 +++++++++++++++++++--------------------- tests/unit/test_grid.py | 28 ++++++------- 2 files changed, 55 insertions(+), 60 deletions(-) diff --git a/src/darsia/utils/grid.py b/src/darsia/utils/grid.py index a7abd4d9..0ac77958 100644 --- a/src/darsia/utils/grid.py +++ b/src/darsia/utils/grid.py @@ -12,10 +12,8 @@ class Grid: """Tensor grid. - Attributes: - shape: Shape of grid. - ndim: Number of dimensions. - size: Number of grid points. + The current implmentatio does nt take into accout true boundary faces assuming + the boundar values are not of interest. """ @@ -67,40 +65,47 @@ def _setup(self) -> None: ] """list: Shape of inner faces in each axis.""" - self.num_inner_faces = [np.prod(s) for s in self.inner_faces_shape] + self.num_faces_per_axis = [np.prod(s) for s in self.inner_faces_shape] """list: Number of inner faces in each axis.""" - self.num_faces = np.sum(self.num_inner_faces) + self.num_faces = np.sum(self.num_faces_per_axis) """int: Number of faces.""" # Define indexing and ordering of inner faces. Horizontal -> vertical -> depth. # TODO replace with slices - self.flat_inner_faces = [ - sum(self.num_inner_faces[:d]) - + np.arange(self.num_inner_faces[d], dtype=int) + self.faces = [ + sum(self.num_faces_per_axis[:d]) + + np.arange(self.num_faces_per_axis[d], dtype=int) for d in range(self.dim) ] - self.inner_faces = [ - self.flat_inner_faces[d].reshape(self.inner_faces_shape[d]) + self.faces_slice = [ + slice( + sum(self.num_faces_per_axis[:d]), + None if d == self.dim - 1 else sum(self.num_faces_per_axis[: d + 1]), + ) for d in range(self.dim) ] + self.face_index = [ + self.faces[d].reshape(self.inner_faces_shape[d]) for d in range(self.dim) + ] + # Identify inner faces (full cube) if self.dim == 1: - self.interior_inner_faces = [ - np.ravel(self.inner_faces[0][1:-1]), + self.interior_faces = [ + np.ravel(self.face_index[0][1:-1]), ] elif self.dim == 2: - self.interior_inner_faces = [ - np.ravel(self.inner_faces[0][:, 1:-1]), - np.ravel(self.inner_faces[1][1:-1, :]), + self.interior_faces = [ + np.ravel(self.face_index[0][:, 1:-1]), + np.ravel(self.face_index[1][1:-1, :]), ] elif self.dim == 3: - self.interior_inner_faces = [ - np.ravel(self.inner_faces[0][:, 1:-1, 1:-1]), - np.ravel(self.inner_faces[1][1:-1, :, 1:-1]), - np.ravel(self.inner_faces[2][1:-1, 1:-1, :]), + self.interior_faces = [ + np.ravel(self.face_index[0][:, 1:-1, 1:-1]), + np.ravel(self.face_index[1][1:-1, :, 1:-1]), + np.ravel(self.face_index[2][1:-1, 1:-1, :]), ] else: raise NotImplementedError(f"Grid of dimension {self.dim} not implemented.") @@ -108,13 +113,11 @@ def _setup(self) -> None: # Identify all faces on the outer boundary of the grid. Need to use hardcoded # knowledge of the orientation of axes and grid indexing. if self.dim == 1: - self.exterior_inner_faces = [ - np.ravel(self.inner_faces[0][np.array([0, -1])]) - ] + self.exterior_faces = [np.ravel(self.face_index[0][np.array([0, -1])])] elif self.dim == 2: - self.exterior_inner_faces = [ - np.ravel(self.inner_faces[0][:, np.array([0, -1])]), - np.ravel(self.inner_faces[1][np.array([0, -1]), :]), + self.exterior_faces = [ + np.ravel(self.face_index[0][:, np.array([0, -1])]), + np.ravel(self.face_index[1][np.array([0, -1]), :]), ] elif self.dim == 3: # TODO @@ -128,24 +131,16 @@ def _setup(self) -> None: self.connectivity = np.zeros((self.num_faces, 2), dtype=int) """np.ndarray: Connectivity (and direction) of faces to cells.""" if self.dim >= 1: - self.connectivity[self.flat_inner_faces[0], 0] = np.ravel( - self.cell_index[:-1, ...] - ) - self.connectivity[self.flat_inner_faces[0], 1] = np.ravel( - self.cell_index[1:, ...] - ) + self.connectivity[self.faces[0], 0] = np.ravel(self.cell_index[:-1, ...]) + self.connectivity[self.faces[0], 1] = np.ravel(self.cell_index[1:, ...]) if self.dim >= 2: - self.connectivity[self.flat_inner_faces[1], 0] = np.ravel( - self.cell_index[:, :-1, ...] - ) - self.connectivity[self.flat_inner_faces[1], 1] = np.ravel( - self.cell_index[:, 1:, ...] - ) + self.connectivity[self.faces[1], 0] = np.ravel(self.cell_index[:, :-1, ...]) + self.connectivity[self.faces[1], 1] = np.ravel(self.cell_index[:, 1:, ...]) if self.dim >= 3: - self.connectivity[self.flat_inner_faces[2], 0] = np.ravel( + self.connectivity[self.faces[2], 0] = np.ravel( self.cell_index[:, :, -1, ...] ) - self.connectivity[self.flat_inner_faces[2], 1] = np.ravel( + self.connectivity[self.faces[2], 1] = np.ravel( self.cell_index[:, :, 1:, ...] ) if self.dim > 3: @@ -161,26 +156,26 @@ def _setup(self) -> None: if self.dim >= 1: self.reverse_connectivity[ 0, np.ravel(self.cell_index[1:, ...]), 0 - ] = self.flat_inner_faces[0] + ] = self.faces[0] self.reverse_connectivity[ 0, np.ravel(self.cell_index[:-1, ...]), 1 - ] = self.flat_inner_faces[0] + ] = self.faces[0] if self.dim >= 2: self.reverse_connectivity[ 1, np.ravel(self.cell_index[:, 1:, ...]), 0 - ] = self.flat_inner_faces[1] + ] = self.faces[1] self.reverse_connectivity[ 1, np.ravel(self.cell_index[:, :-1, ...]), 1 - ] = self.flat_inner_faces[1] + ] = self.faces[1] if self.dim >= 3: self.reverse_connectivity[ 2, np.ravel(self.cell_index[:, :, 1:, ...]), 0 - ] = self.flat_inner_faces[2] + ] = self.faces[2] self.reverse_connectivity[ 2, np.ravel(self.cell_index[:, :, :-1, ...]), 1 - ] = self.flat_inner_faces[2] + ] = self.faces[2] # Info about inner cells # TODO rm? diff --git a/tests/unit/test_grid.py b/tests/unit/test_grid.py index 3d0c5efd..bafec54b 100644 --- a/tests/unit/test_grid.py +++ b/tests/unit/test_grid.py @@ -28,29 +28,29 @@ def test_grid_2d(): assert np.allclose(grid.inner_faces_shape[1], (4, 4)) # Check face numbering - assert grid.num_inner_faces[0] == 15 - assert grid.num_inner_faces[1] == 16 + assert grid.num_faces_per_axis[0] == 15 + assert grid.num_faces_per_axis[1] == 16 assert grid.num_faces == 15 + 16 # Check indexing of faces in flat format - assert np.allclose(grid.flat_inner_faces[0], np.arange(0, 15)) - assert np.allclose(grid.flat_inner_faces[1], np.arange(15, 31)) + assert np.allclose(grid.faces[0], np.arange(0, 15)) + assert np.allclose(grid.faces[1], np.arange(15, 31)) # Check indexing of faces in 2d format - assert np.allclose(grid.inner_faces[0][0], np.arange(0, 5)) - assert np.allclose(grid.inner_faces[0][1], np.arange(5, 10)) - assert np.allclose(grid.inner_faces[0][2], np.arange(10, 15)) - assert np.allclose(grid.inner_faces[1][0], np.arange(15, 19)) - assert np.allclose(grid.inner_faces[1][1], np.arange(19, 23)) - assert np.allclose(grid.inner_faces[1][2], np.arange(23, 27)) + assert np.allclose(grid.face_index[0][0], np.arange(0, 5)) + assert np.allclose(grid.face_index[0][1], np.arange(5, 10)) + assert np.allclose(grid.face_index[0][2], np.arange(10, 15)) + assert np.allclose(grid.face_index[1][0], np.arange(15, 19)) + assert np.allclose(grid.face_index[1][1], np.arange(19, 23)) + assert np.allclose(grid.face_index[1][2], np.arange(23, 27)) # Check identification of interior inner faces - assert np.allclose(grid.interior_inner_faces[0], [1, 2, 3, 6, 7, 8, 11, 12, 13]) - assert np.allclose(grid.interior_inner_faces[1], [19, 20, 21, 22, 23, 24, 25, 26]) + assert np.allclose(grid.interior_faces[0], [1, 2, 3, 6, 7, 8, 11, 12, 13]) + assert np.allclose(grid.interior_faces[1], [19, 20, 21, 22, 23, 24, 25, 26]) # Check identification of exterior inner faces - assert np.allclose(grid.exterior_inner_faces[0], [0, 4, 5, 9, 10, 14]) - assert np.allclose(grid.exterior_inner_faces[1], [15, 16, 17, 18, 27, 28, 29, 30]) + assert np.allclose(grid.exterior_faces[0], [0, 4, 5, 9, 10, 14]) + assert np.allclose(grid.exterior_faces[1], [15, 16, 17, 18, 27, 28, 29, 30]) # Check connectivity: face to cells with positive orientation assert np.allclose(grid.connectivity[0], [0, 5]) From 11f362dd78a669d8b2e8cd8f6659c2329bc04baa Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Tue, 19 Sep 2023 13:26:38 +0200 Subject: [PATCH 083/100] DOC: Add more attribute descriptions. --- src/darsia/utils/grid.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/darsia/utils/grid.py b/src/darsia/utils/grid.py index 0ac77958..253c603d 100644 --- a/src/darsia/utils/grid.py +++ b/src/darsia/utils/grid.py @@ -78,20 +78,17 @@ def _setup(self) -> None: + np.arange(self.num_faces_per_axis[d], dtype=int) for d in range(self.dim) ] - - self.faces_slice = [ - slice( - sum(self.num_faces_per_axis[:d]), - None if d == self.dim - 1 else sum(self.num_faces_per_axis[: d + 1]), - ) - for d in range(self.dim) - ] + """list: Indices of inner faces in each axis.""" self.face_index = [ self.faces[d].reshape(self.inner_faces_shape[d]) for d in range(self.dim) ] + """list: Indices of inner faces in each axis, using matrix indexing.""" # Identify inner faces (full cube) + self.interior_faces = [] + """list: Indices of interior faces.""" + if self.dim == 1: self.interior_faces = [ np.ravel(self.face_index[0][1:-1]), @@ -112,6 +109,9 @@ def _setup(self) -> None: # Identify all faces on the outer boundary of the grid. Need to use hardcoded # knowledge of the orientation of axes and grid indexing. + self.exterior_faces = [] + """list: Indices of exterior faces (inner faces of boundary cells).""" + if self.dim == 1: self.exterior_faces = [np.ravel(self.face_index[0][np.array([0, -1])])] elif self.dim == 2: @@ -130,6 +130,7 @@ def _setup(self) -> None: self.connectivity = np.zeros((self.num_faces, 2), dtype=int) """np.ndarray: Connectivity (and direction) of faces to cells.""" + if self.dim >= 1: self.connectivity[self.faces[0], 0] = np.ravel(self.cell_index[:-1, ...]) self.connectivity[self.faces[0], 1] = np.ravel(self.cell_index[1:, ...]) @@ -188,6 +189,7 @@ def _setup(self) -> None: if self.dim >= 3 else [] ) + """list: Indices of inner cells with inner faces.""" def generate_grid(image: darsia.Image) -> Grid: From cb7c0dfe0f925818a324db92dbc1b6017442f25b Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Tue, 19 Sep 2023 13:28:57 +0200 Subject: [PATCH 084/100] MAINT: Adapt variable renaming. --- src/darsia/utils/fv.py | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/src/darsia/utils/fv.py b/src/darsia/utils/fv.py index 08a7a185..518c4b59 100644 --- a/src/darsia/utils/fv.py +++ b/src/darsia/utils/fv.py @@ -21,15 +21,12 @@ def __init__(self, grid: darsia.Grid) -> None: div_shape = (grid.num_cells, grid.num_faces) div_data = np.concatenate( [ - grid.face_vol[d] * np.tile([1, -1], grid.num_inner_faces[d]) + grid.face_vol[d] * np.tile([1, -1], grid.num_faces_per_axis[d]) for d in range(grid.dim) ] ) div_row = np.concatenate( - [ - np.ravel(grid.connectivity[grid.flat_inner_faces[d]]) - for d in range(grid.dim) - ] + [np.ravel(grid.connectivity[grid.faces[d]]) for d in range(grid.dim)] ) div_col = np.repeat(np.arange(grid.num_faces, dtype=int), 2) div = sps.csc_matrix( @@ -181,8 +178,8 @@ def __init__(self, grid: darsia.Grid) -> None: data = np.tile( 0.25 * np.ones( - 2 * np.array(grid.exterior_inner_faces).size - + 4 * np.array(grid.interior_inner_faces).size, + 2 * np.array(grid.exterior_faces).size + + 4 * np.array(grid.interior_faces).size, dtype=float, ), grid.dim - 1, @@ -191,11 +188,11 @@ def __init__(self, grid: darsia.Grid) -> None: # The rows correspond to the faces for which the tangential fluxes are # determined times the component of the tangential fluxes. rows_outer = np.concatenate( - [np.repeat(grid.exterior_inner_faces[d], 2) for d in range(grid.dim)] + [np.repeat(grid.exterior_faces[d], 2) for d in range(grid.dim)] ) rows_inner = np.concatenate( - [np.repeat(grid.interior_inner_faces[d], 4) for d in range(grid.dim)] + [np.repeat(grid.interior_faces[d], 4) for d in range(grid.dim)] ) # The columns correspond to the (orthogonal) faces contributing to the average @@ -215,7 +212,7 @@ def interleaf(a: np.ndarray, b: np.ndarray) -> np.ndarray: np.ravel( grid.reverse_connectivity[ d_perp, - np.ravel(grid.connectivity[grid.exterior_inner_faces[d]]), + np.ravel(grid.connectivity[grid.exterior_faces[d]]), ] ) for d in range(grid.dim) @@ -231,7 +228,7 @@ def interleaf(a: np.ndarray, b: np.ndarray) -> np.ndarray: np.ravel( grid.reverse_connectivity[ d_perp, - np.ravel(grid.connectivity[grid.interior_inner_faces[d]]), + np.ravel(grid.connectivity[grid.interior_faces[d]]), ] ) for d in range(grid.dim) @@ -279,26 +276,26 @@ def face_to_cell(grid: darsia.Grid, flat_flux: np.ndarray) -> np.ndarray: cell_flux = np.zeros((*grid.shape, grid.dim), dtype=float) if grid.dim >= 1: - cell_flux[:-1, ..., 0] += 0.5 * flat_flux[grid.flat_inner_faces[0]].reshape( + cell_flux[:-1, ..., 0] += 0.5 * flat_flux[grid.faces[0]].reshape( grid.inner_faces_shape[0] ) - cell_flux[1:, ..., 0] += 0.5 * flat_flux[grid.flat_inner_faces[0]].reshape( + cell_flux[1:, ..., 0] += 0.5 * flat_flux[grid.faces[0]].reshape( grid.inner_faces_shape[0] ) if grid.dim >= 2: - cell_flux[:, :-1, ..., 1] += 0.5 * flat_flux[grid.flat_inner_faces[1]].reshape( + cell_flux[:, :-1, ..., 1] += 0.5 * flat_flux[grid.faces[1]].reshape( grid.inner_faces_shape[1] ) - cell_flux[:, 1:, ..., 1] += 0.5 * flat_flux[grid.flat_inner_faces[1]].reshape( + cell_flux[:, 1:, ..., 1] += 0.5 * flat_flux[grid.faces[1]].reshape( grid.inner_faces_shape[1] ) if grid.dim >= 3: - cell_flux[:, :, :-1, ..., 2] += 0.5 * flat_flux[ - grid.flat_inner_faces[2] - ].reshape(grid.inner_faces_shape[2]) - cell_flux[:, :, 1:, ..., 2] += 0.5 * flat_flux[ - grid.flat_inner_faces[2] - ].reshape(grid.inner_faces_shape[2]) + cell_flux[:, :, :-1, ..., 2] += 0.5 * flat_flux[grid.faces[2]].reshape( + grid.inner_faces_shape[2] + ) + cell_flux[:, :, 1:, ..., 2] += 0.5 * flat_flux[grid.faces[2]].reshape( + grid.inner_faces_shape[2] + ) if grid.dim > 3: raise NotImplementedError(f"Dimension {grid.dim} not supported.") From 642c47651af820207df4f0201ad30dc9520c890c Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Tue, 19 Sep 2023 19:26:04 +0200 Subject: [PATCH 085/100] ENH: Extend tensor grid utilities to 3d --- src/darsia/utils/grid.py | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/src/darsia/utils/grid.py b/src/darsia/utils/grid.py index 253c603d..0367966e 100644 --- a/src/darsia/utils/grid.py +++ b/src/darsia/utils/grid.py @@ -59,8 +59,21 @@ def _setup(self) -> None: """np.ndarray: cell indices.""" # Determine number of inner faces in each axis + # Flip order of axes to get the correct shape + if self.dim == 1: + order = [np.array([0])] + elif self.dim == 2: + order = [np.array([0, 1]), np.array([0, 1])] + elif self.dim == 3: + order = [ + np.array([0, 1, 2]), + np.array([1, 0, 2]), + np.array([2, 0, 1]), + ] self.inner_faces_shape = [ - tuple(np.array(self.shape) - np.eye(self.dim, dtype=int)[d]) + # tuple(np.roll(np.array(self.shape) - np.eye(self.dim, dtype=int)[d], -d)) + # tuple(np.array(self.shape) - np.eye(self.dim, dtype=int)[d]) + tuple((np.array(self.shape) - np.eye(self.dim, dtype=int)[d])[order[d]]) for d in range(self.dim) ] """list: Shape of inner faces in each axis.""" @@ -101,8 +114,8 @@ def _setup(self) -> None: elif self.dim == 3: self.interior_faces = [ np.ravel(self.face_index[0][:, 1:-1, 1:-1]), - np.ravel(self.face_index[1][1:-1, :, 1:-1]), - np.ravel(self.face_index[2][1:-1, 1:-1, :]), + np.ravel(self.face_index[1][:, 1:-1, 1:-1]), + np.ravel(self.face_index[2][:, 1:-1, 1:-1]), ] else: raise NotImplementedError(f"Grid of dimension {self.dim} not implemented.") @@ -120,9 +133,18 @@ def _setup(self) -> None: np.ravel(self.face_index[1][np.array([0, -1]), :]), ] elif self.dim == 3: - # TODO - raise NotImplementedError - self.outer_faces = [] + # Extract boundary elements of each axis + self.exterior_faces = [ + np.sort( + np.concatenate( + ( + np.ravel(self.face_index[d][:, np.array([0, -1]), :]), + np.ravel(self.face_index[d][:, 1:-1, np.array([0, -1])]), + ) + ) + ) + for d in range(self.dim) + ] else: raise NotImplementedError(f"Grid of dimension {self.dim} not implemented.") @@ -139,7 +161,7 @@ def _setup(self) -> None: self.connectivity[self.faces[1], 1] = np.ravel(self.cell_index[:, 1:, ...]) if self.dim >= 3: self.connectivity[self.faces[2], 0] = np.ravel( - self.cell_index[:, :, -1, ...] + self.cell_index[:, :, :-1, ...] ) self.connectivity[self.faces[2], 1] = np.ravel( self.cell_index[:, :, 1:, ...] From 6e3f2dcff68520f3d718e2d12e80b247e25bb9eb Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Tue, 19 Sep 2023 19:26:20 +0200 Subject: [PATCH 086/100] TST: Add 3d grid test --- tests/unit/test_grid.py | 212 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) diff --git a/tests/unit/test_grid.py b/tests/unit/test_grid.py index bafec54b..6640091d 100644 --- a/tests/unit/test_grid.py +++ b/tests/unit/test_grid.py @@ -76,3 +76,215 @@ def test_grid_2d(): # For interior cells assert np.allclose(grid.reverse_connectivity[0, 6], [1, 6]) assert np.allclose(grid.reverse_connectivity[1, 6], [19, 20]) + + +def test_grid_3d(): + grid = darsia.Grid(shape=(3, 4, 5), voxel_size=[0.5, 0.25, 2]) + + # Check basic attributes + assert np.allclose(grid.shape, (3, 4, 5)) + assert grid.dim == 3 + assert np.allclose(grid.voxel_size, [0.5, 0.25, 2]) + + # Probe cell numbering + assert grid.num_cells == 60 + assert grid.cell_index[0, 0, 0] == 0 + assert grid.cell_index[2, 0, 0] == 40 + assert grid.cell_index[0, 3, 0] == 15 + assert grid.cell_index[2, 3, 0] == 55 + assert grid.cell_index[0, 0, 4] == 4 + assert grid.cell_index[2, 0, 4] == 44 + assert grid.cell_index[0, 3, 4] == 19 + assert grid.cell_index[2, 3, 4] == 59 + + # Check face volumes + assert np.allclose(grid.face_vol, [0.5, 1, 0.5 * 0.25]) + + # Check face shape + assert np.allclose(grid.inner_faces_shape[0], (2, 4, 5)) + assert np.allclose(grid.inner_faces_shape[1], (3, 3, 5)) + assert np.allclose(grid.inner_faces_shape[2], (4, 3, 4)) + + # Check face numbering + assert grid.num_faces_per_axis[0] == 40 + assert grid.num_faces_per_axis[1] == 45 + assert grid.num_faces_per_axis[2] == 48 + assert grid.num_faces == 40 + 45 + 48 + + # Check indexing of faces in flat format + assert np.allclose(grid.faces[0], np.arange(0, 40)) + assert np.allclose(grid.faces[1], np.arange(40, 40 + 45)) + assert np.allclose(grid.faces[2], np.arange(40 + 45, 40 + 45 + 48)) + + # Check indexing of faces in 3d format + assert np.allclose(grid.face_index[0][0, 0], np.arange(0, 5)) + assert np.allclose(grid.face_index[0][1, 0], np.arange(20, 25)) + assert np.allclose(grid.face_index[0][0, 1], np.arange(5, 10)) + assert np.allclose(grid.face_index[0][1, 1], np.arange(25, 30)) + # ... + assert np.allclose(grid.face_index[1][0, 0], np.arange(40, 45)) + assert np.allclose(grid.face_index[1][1, 0], np.arange(55, 60)) + assert np.allclose(grid.face_index[1][2, 0], np.arange(70, 75)) + assert np.allclose(grid.face_index[1][0, 1], np.arange(45, 50)) + # ... + assert np.allclose(grid.face_index[2][0, 0], np.arange(85, 89)) + assert np.allclose(grid.face_index[2][1, 0], np.arange(97, 101)) + assert np.allclose(grid.face_index[2][2, 0], np.arange(109, 113)) + assert np.allclose(grid.face_index[2][0, 1], np.arange(89, 93)) + # ... + + # Check identification of interior inner faces + assert np.allclose( + grid.interior_faces[0], [6, 7, 8, 11, 12, 13, 26, 27, 28, 31, 32, 33] + ) + assert np.allclose(grid.interior_faces[1], [46, 47, 48, 61, 62, 63, 76, 77, 78]) + assert np.allclose(grid.interior_faces[2], [90, 91, 102, 103, 114, 115, 126, 127]) + + # Check identification of exterior inner faces + assert np.allclose( + grid.exterior_faces[0], + [ + 0, + 1, + 2, + 3, + 4, + 5, + 9, + 10, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 29, + 30, + 34, + 35, + 36, + 37, + 38, + 39, + ], + ) + assert np.allclose( + grid.exterior_faces[1], + [ + 40, + 41, + 42, + 43, + 44, + 45, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 79, + 80, + 81, + 82, + 83, + 84, + ], + ) + assert np.allclose( + grid.exterior_faces[2], + [ + 85, + 86, + 87, + 88, + 89, + 92, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + 101, + 104, + 105, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 116, + 117, + 118, + 119, + 120, + 121, + 122, + 123, + 124, + 125, + 128, + 129, + 130, + 131, + 132, + ], + ) + + # Check connectivity: face to cells with positive orientation + # across 0-normal: + assert np.allclose(grid.connectivity[0], [0, 20]) + assert np.allclose(grid.connectivity[39], [39, 59]) + # across 1-normal: + assert np.allclose(grid.connectivity[40], [0, 5]) + assert np.allclose(grid.connectivity[84], [54, 59]) + # across 2-normal: + assert np.allclose(grid.connectivity[85], [0, 1]) + assert np.allclose(grid.connectivity[132], [58, 59]) + + # Check reverse connectivity: cell to faces with positive orientation + # For corner cells + assert np.allclose(grid.reverse_connectivity[0, 0], [-1, 0]) + assert np.allclose(grid.reverse_connectivity[1, 0], [-1, 40]) + assert np.allclose(grid.reverse_connectivity[2, 0], [-1, 85]) + assert np.allclose(grid.reverse_connectivity[0, 19], [-1, 19]) + assert np.allclose(grid.reverse_connectivity[1, 19], [54, -1]) + assert np.allclose(grid.reverse_connectivity[2, 19], [100, -1]) + assert np.allclose(grid.reverse_connectivity[0, 59], [39, -1]) + assert np.allclose(grid.reverse_connectivity[1, 59], [84, -1]) + assert np.allclose(grid.reverse_connectivity[2, 59], [132, -1]) + + # For interior cells + assert np.allclose(grid.reverse_connectivity[0, 26], [6, 26]) + assert np.allclose(grid.reverse_connectivity[1, 26], [56, 61]) + assert np.allclose(grid.reverse_connectivity[2, 26], [105, 106]) From 8eba560a363a6d8ea2fb9d9e058056f7ec138268 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Tue, 19 Sep 2023 23:22:29 +0200 Subject: [PATCH 087/100] MAINT: Apply consistent ordering of faces and cells. --- src/darsia/utils/grid.py | 113 ++++++++++++++++----------------------- 1 file changed, 45 insertions(+), 68 deletions(-) diff --git a/src/darsia/utils/grid.py b/src/darsia/utils/grid.py index 0367966e..6a921bcc 100644 --- a/src/darsia/utils/grid.py +++ b/src/darsia/utils/grid.py @@ -55,30 +55,19 @@ def _setup(self) -> None: self.num_cells = np.prod(self.shape) """int: Number of cells.""" - self.cell_index = np.arange(self.num_cells, dtype=int).reshape(self.shape) - """np.ndarray: cell indices.""" + self.cell_index = np.arange(self.num_cells, dtype=int).reshape( + self.shape, order="F" + ) + """np.ndarray: cell indices, following the matrix indexing convention.""" # Determine number of inner faces in each axis - # Flip order of axes to get the correct shape - if self.dim == 1: - order = [np.array([0])] - elif self.dim == 2: - order = [np.array([0, 1]), np.array([0, 1])] - elif self.dim == 3: - order = [ - np.array([0, 1, 2]), - np.array([1, 0, 2]), - np.array([2, 0, 1]), - ] - self.inner_faces_shape = [ - # tuple(np.roll(np.array(self.shape) - np.eye(self.dim, dtype=int)[d], -d)) - # tuple(np.array(self.shape) - np.eye(self.dim, dtype=int)[d]) - tuple((np.array(self.shape) - np.eye(self.dim, dtype=int)[d])[order[d]]) + self.faces_shape = [ + np.array(self.shape) - np.eye(self.dim, dtype=int)[d] for d in range(self.dim) ] """list: Shape of inner faces in each axis.""" - self.num_faces_per_axis = [np.prod(s) for s in self.inner_faces_shape] + self.num_faces_per_axis = [np.prod(s) for s in self.faces_shape] """list: Number of inner faces in each axis.""" self.num_faces = np.sum(self.num_faces_per_axis) @@ -94,7 +83,8 @@ def _setup(self) -> None: """list: Indices of inner faces in each axis.""" self.face_index = [ - self.faces[d].reshape(self.inner_faces_shape[d]) for d in range(self.dim) + self.faces[d].reshape(self.faces_shape[d], order="F") + for d in range(self.dim) ] """list: Indices of inner faces in each axis, using matrix indexing.""" @@ -104,49 +94,28 @@ def _setup(self) -> None: if self.dim == 1: self.interior_faces = [ - np.ravel(self.face_index[0][1:-1]), + np.ravel(self.face_index[0][1:-1], "F"), ] elif self.dim == 2: self.interior_faces = [ - np.ravel(self.face_index[0][:, 1:-1]), - np.ravel(self.face_index[1][1:-1, :]), + np.ravel(self.face_index[0][:, 1:-1], "F"), + np.ravel(self.face_index[1][1:-1, :], "F"), ] elif self.dim == 3: self.interior_faces = [ - np.ravel(self.face_index[0][:, 1:-1, 1:-1]), - np.ravel(self.face_index[1][:, 1:-1, 1:-1]), - np.ravel(self.face_index[2][:, 1:-1, 1:-1]), + np.ravel(self.face_index[0][:, 1:-1, 1:-1], "F"), + np.ravel(self.face_index[1][1:-1, :, 1:-1], "F"), + np.ravel(self.face_index[2][1:-1, 1:-1, :], "F"), ] else: raise NotImplementedError(f"Grid of dimension {self.dim} not implemented.") # Identify all faces on the outer boundary of the grid. Need to use hardcoded # knowledge of the orientation of axes and grid indexing. - self.exterior_faces = [] - """list: Indices of exterior faces (inner faces of boundary cells).""" - - if self.dim == 1: - self.exterior_faces = [np.ravel(self.face_index[0][np.array([0, -1])])] - elif self.dim == 2: - self.exterior_faces = [ - np.ravel(self.face_index[0][:, np.array([0, -1])]), - np.ravel(self.face_index[1][np.array([0, -1]), :]), - ] - elif self.dim == 3: - # Extract boundary elements of each axis - self.exterior_faces = [ - np.sort( - np.concatenate( - ( - np.ravel(self.face_index[d][:, np.array([0, -1]), :]), - np.ravel(self.face_index[d][:, 1:-1, np.array([0, -1])]), - ) - ) - ) - for d in range(self.dim) - ] - else: - raise NotImplementedError(f"Grid of dimension {self.dim} not implemented.") + self.exterior_faces = [ + np.sort(np.array(list(set(self.faces[d]) - set(self.interior_faces[d])))) + for d in range(self.dim) + ] # ! ---- Connectivity ---- @@ -154,17 +123,25 @@ def _setup(self) -> None: """np.ndarray: Connectivity (and direction) of faces to cells.""" if self.dim >= 1: - self.connectivity[self.faces[0], 0] = np.ravel(self.cell_index[:-1, ...]) - self.connectivity[self.faces[0], 1] = np.ravel(self.cell_index[1:, ...]) + self.connectivity[self.faces[0], 0] = np.ravel( + self.cell_index[:-1, ...], "F" + ) + self.connectivity[self.faces[0], 1] = np.ravel( + self.cell_index[1:, ...], "F" + ) if self.dim >= 2: - self.connectivity[self.faces[1], 0] = np.ravel(self.cell_index[:, :-1, ...]) - self.connectivity[self.faces[1], 1] = np.ravel(self.cell_index[:, 1:, ...]) + self.connectivity[self.faces[1], 0] = np.ravel( + self.cell_index[:, :-1, ...], "F" + ) + self.connectivity[self.faces[1], 1] = np.ravel( + self.cell_index[:, 1:, ...], "F" + ) if self.dim >= 3: self.connectivity[self.faces[2], 0] = np.ravel( - self.cell_index[:, :, :-1, ...] + self.cell_index[:, :, :-1, ...], "F" ) self.connectivity[self.faces[2], 1] = np.ravel( - self.cell_index[:, :, 1:, ...] + self.cell_index[:, :, 1:, ...], "F" ) if self.dim > 3: raise NotImplementedError(f"Grid of dimension {self.dim} not implemented.") @@ -172,42 +149,42 @@ def _setup(self) -> None: self.reverse_connectivity = -np.ones((self.dim, self.num_cells, 2), dtype=int) """np.ndarray: Reverse connectivity (and direction) of cells to faces.""" - # NOTE: The first components addresses the cell, the second the axis, the third - # the direction of the relative position of the face wrt the cell (0: left/up, - # 1: right/down, using matrix indexing in 2d - analogously in 3d). + # NOTE: The first components addresses the normal direction of the face, + # the second the cell, the third the relative position of the face in the + # cell. if self.dim >= 1: self.reverse_connectivity[ - 0, np.ravel(self.cell_index[1:, ...]), 0 + 0, np.ravel(self.cell_index[1:, ...], "F"), 0 ] = self.faces[0] self.reverse_connectivity[ - 0, np.ravel(self.cell_index[:-1, ...]), 1 + 0, np.ravel(self.cell_index[:-1, ...], "F"), 1 ] = self.faces[0] if self.dim >= 2: self.reverse_connectivity[ - 1, np.ravel(self.cell_index[:, 1:, ...]), 0 + 1, np.ravel(self.cell_index[:, 1:, ...], "F"), 0 ] = self.faces[1] self.reverse_connectivity[ - 1, np.ravel(self.cell_index[:, :-1, ...]), 1 + 1, np.ravel(self.cell_index[:, :-1, ...], "F"), 1 ] = self.faces[1] if self.dim >= 3: self.reverse_connectivity[ - 2, np.ravel(self.cell_index[:, :, 1:, ...]), 0 + 2, np.ravel(self.cell_index[:, :, 1:, ...], "F"), 0 ] = self.faces[2] self.reverse_connectivity[ - 2, np.ravel(self.cell_index[:, :, :-1, ...]), 1 + 2, np.ravel(self.cell_index[:, :, :-1, ...], "F"), 1 ] = self.faces[2] # Info about inner cells # TODO rm? self.inner_cells_with_inner_faces = ( - [] + [np.ravel(self.cell_index[1:-1, ...])] + [] + [np.ravel(self.cell_index[1:-1, ...], "F")] if self.dim >= 1 - else [] + [np.ravel(self.cell_index[:, 1:-1, ...])] + else [] + [np.ravel(self.cell_index[:, 1:-1, ...], "F")] if self.dim >= 2 - else [] + [np.ravel(self.cell_index[:, :, 1:-1, ...])] + else [] + [np.ravel(self.cell_index[:, :, 1:-1, ...], "F")] if self.dim >= 3 else [] ) From 1dfd86bb25617297519249e370e7f6346162194e Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Tue, 19 Sep 2023 23:22:54 +0200 Subject: [PATCH 088/100] TST: Adapt test new consistent indexing. --- tests/unit/test_grid.py | 379 +++++++++++++++++++--------------------- 1 file changed, 182 insertions(+), 197 deletions(-) diff --git a/tests/unit/test_grid.py b/tests/unit/test_grid.py index 6640091d..643b5596 100644 --- a/tests/unit/test_grid.py +++ b/tests/unit/test_grid.py @@ -1,84 +1,109 @@ """Unit tests for the grid module.""" import numpy as np +import pytest import darsia def test_grid_2d(): - grid = darsia.Grid(shape=(4, 5), voxel_size=[0.5, 0.25]) + """Test basic indexing for 2d tensor grids.""" + + # Fetch grid + grid = darsia.Grid(shape=(3, 4), voxel_size=[0.5, 0.25]) # Check basic attributes - assert np.allclose(grid.shape, (4, 5)) + assert np.allclose(grid.shape, (3, 4)) assert grid.dim == 2 assert np.allclose(grid.voxel_size, [0.5, 0.25]) # Probe cell numbering - assert grid.num_cells == 20 + assert grid.num_cells == 12 assert grid.cell_index[0, 0] == 0 - assert grid.cell_index[0, 4] == 4 - assert grid.cell_index[3, 0] == 15 - assert grid.cell_index[3, 4] == 19 + assert grid.cell_index[2, 0] == 2 + assert grid.cell_index[0, 3] == 9 + assert grid.cell_index[2, 3] == 11 # Check face volumes assert np.allclose(grid.face_vol, [0.25, 0.5]) # Check face shape - assert np.allclose(grid.inner_faces_shape[0], (3, 5)) - assert np.allclose(grid.inner_faces_shape[1], (4, 4)) + assert np.allclose(grid.faces_shape[0], (2, 4)) + assert np.allclose(grid.faces_shape[1], (3, 3)) # Check face numbering - assert grid.num_faces_per_axis[0] == 15 - assert grid.num_faces_per_axis[1] == 16 - assert grid.num_faces == 15 + 16 + assert grid.num_faces_per_axis[0] == 8 + assert grid.num_faces_per_axis[1] == 9 + assert grid.num_faces == 17 # Check indexing of faces in flat format - assert np.allclose(grid.faces[0], np.arange(0, 15)) - assert np.allclose(grid.faces[1], np.arange(15, 31)) + assert np.allclose(grid.faces[0], np.arange(0, 8)) + assert np.allclose(grid.faces[1], np.arange(8, 17)) # Check indexing of faces in 2d format - assert np.allclose(grid.face_index[0][0], np.arange(0, 5)) - assert np.allclose(grid.face_index[0][1], np.arange(5, 10)) - assert np.allclose(grid.face_index[0][2], np.arange(10, 15)) - assert np.allclose(grid.face_index[1][0], np.arange(15, 19)) - assert np.allclose(grid.face_index[1][1], np.arange(19, 23)) - assert np.allclose(grid.face_index[1][2], np.arange(23, 27)) + assert np.allclose(grid.face_index[0][:, 0], np.arange(0, 2)) + assert np.allclose(grid.face_index[0][:, 1], np.arange(2, 4)) + assert np.allclose(grid.face_index[0][:, 2], np.arange(4, 6)) + assert np.allclose(grid.face_index[1][:, 0], np.arange(8, 11)) + assert np.allclose(grid.face_index[1][:, 1], np.arange(11, 14)) + assert np.allclose(grid.face_index[1][:, 2], np.arange(14, 17)) # Check identification of interior inner faces - assert np.allclose(grid.interior_faces[0], [1, 2, 3, 6, 7, 8, 11, 12, 13]) - assert np.allclose(grid.interior_faces[1], [19, 20, 21, 22, 23, 24, 25, 26]) + assert np.allclose(grid.interior_faces[0], [2, 3, 4, 5]) + assert np.allclose(grid.interior_faces[1], [9, 12, 15]) # Check identification of exterior inner faces - assert np.allclose(grid.exterior_faces[0], [0, 4, 5, 9, 10, 14]) - assert np.allclose(grid.exterior_faces[1], [15, 16, 17, 18, 27, 28, 29, 30]) + assert np.allclose(grid.exterior_faces[0], [0, 1, 6, 7]) + assert np.allclose(grid.exterior_faces[1], [8, 10, 11, 13, 14, 16]) + + +def test_grid_connectivity_2d(): + """Test connectivity for 2d tensor grids.""" + + # Fetch grid + grid = darsia.Grid(shape=(3, 4)) # Check connectivity: face to cells with positive orientation - assert np.allclose(grid.connectivity[0], [0, 5]) - assert np.allclose(grid.connectivity[4], [4, 9]) - assert np.allclose(grid.connectivity[10], [10, 15]) - assert np.allclose(grid.connectivity[14], [14, 19]) - assert np.allclose(grid.connectivity[15], [0, 1]) - assert np.allclose(grid.connectivity[18], [3, 4]) - assert np.allclose(grid.connectivity[27], [15, 16]) - assert np.allclose(grid.connectivity[30], [18, 19]) + # Horizontal faces + assert np.allclose(grid.connectivity[0], [0, 1]) + assert np.allclose(grid.connectivity[1], [1, 2]) + assert np.allclose(grid.connectivity[2], [3, 4]) + assert np.allclose(grid.connectivity[3], [4, 5]) + assert np.allclose(grid.connectivity[4], [6, 7]) + assert np.allclose(grid.connectivity[5], [7, 8]) + assert np.allclose(grid.connectivity[6], [9, 10]) + assert np.allclose(grid.connectivity[7], [10, 11]) + # Vertical faces + assert np.allclose(grid.connectivity[8], [0, 3]) + assert np.allclose(grid.connectivity[9], [1, 4]) + assert np.allclose(grid.connectivity[10], [2, 5]) + assert np.allclose(grid.connectivity[11], [3, 6]) + assert np.allclose(grid.connectivity[12], [4, 7]) + assert np.allclose(grid.connectivity[13], [5, 8]) + assert np.allclose(grid.connectivity[14], [6, 9]) + assert np.allclose(grid.connectivity[15], [7, 10]) + assert np.allclose(grid.connectivity[16], [8, 11]) # Check reverse connectivity: cell to faces with positive orientation # For corner cells assert np.allclose(grid.reverse_connectivity[0, 0], [-1, 0]) - assert np.allclose(grid.reverse_connectivity[1, 0], [-1, 15]) - assert np.allclose(grid.reverse_connectivity[0, 4], [-1, 4]) - assert np.allclose(grid.reverse_connectivity[1, 4], [18, -1]) - assert np.allclose(grid.reverse_connectivity[0, 15], [10, -1]) - assert np.allclose(grid.reverse_connectivity[1, 15], [-1, 27]) - assert np.allclose(grid.reverse_connectivity[0, 19], [14, -1]) - assert np.allclose(grid.reverse_connectivity[1, 19], [30, -1]) + assert np.allclose(grid.reverse_connectivity[1, 0], [-1, 8]) + assert np.allclose(grid.reverse_connectivity[0, 2], [1, -1]) + assert np.allclose(grid.reverse_connectivity[1, 2], [-1, 10]) + assert np.allclose(grid.reverse_connectivity[0, 9], [-1, 6]) + assert np.allclose(grid.reverse_connectivity[1, 9], [14, -1]) + assert np.allclose(grid.reverse_connectivity[0, 11], [7, -1]) + assert np.allclose(grid.reverse_connectivity[1, 11], [16, -1]) # For interior cells - assert np.allclose(grid.reverse_connectivity[0, 6], [1, 6]) - assert np.allclose(grid.reverse_connectivity[1, 6], [19, 20]) + assert np.allclose(grid.reverse_connectivity[0, 4], [2, 3]) + assert np.allclose(grid.reverse_connectivity[1, 4], [9, 12]) def test_grid_3d(): + """Test basic indexing for 2d tensor grids.""" + + # Fetch grid grid = darsia.Grid(shape=(3, 4, 5), voxel_size=[0.5, 0.25, 2]) # Check basic attributes @@ -89,21 +114,21 @@ def test_grid_3d(): # Probe cell numbering assert grid.num_cells == 60 assert grid.cell_index[0, 0, 0] == 0 - assert grid.cell_index[2, 0, 0] == 40 - assert grid.cell_index[0, 3, 0] == 15 - assert grid.cell_index[2, 3, 0] == 55 - assert grid.cell_index[0, 0, 4] == 4 - assert grid.cell_index[2, 0, 4] == 44 - assert grid.cell_index[0, 3, 4] == 19 + assert grid.cell_index[2, 0, 0] == 2 + assert grid.cell_index[0, 3, 0] == 9 + assert grid.cell_index[2, 3, 0] == 11 + assert grid.cell_index[0, 0, 4] == 48 + assert grid.cell_index[2, 0, 4] == 50 + assert grid.cell_index[0, 3, 4] == 57 assert grid.cell_index[2, 3, 4] == 59 # Check face volumes - assert np.allclose(grid.face_vol, [0.5, 1, 0.5 * 0.25]) + assert np.allclose(grid.face_vol, [0.5, 1, 0.125]) # Check face shape - assert np.allclose(grid.inner_faces_shape[0], (2, 4, 5)) - assert np.allclose(grid.inner_faces_shape[1], (3, 3, 5)) - assert np.allclose(grid.inner_faces_shape[2], (4, 3, 4)) + assert np.allclose(grid.faces_shape[0], (2, 4, 5)) + assert np.allclose(grid.faces_shape[1], (3, 3, 5)) + assert np.allclose(grid.faces_shape[2], (3, 4, 4)) # Check face numbering assert grid.num_faces_per_axis[0] == 40 @@ -116,175 +141,135 @@ def test_grid_3d(): assert np.allclose(grid.faces[1], np.arange(40, 40 + 45)) assert np.allclose(grid.faces[2], np.arange(40 + 45, 40 + 45 + 48)) - # Check indexing of faces in 3d format - assert np.allclose(grid.face_index[0][0, 0], np.arange(0, 5)) - assert np.allclose(grid.face_index[0][1, 0], np.arange(20, 25)) - assert np.allclose(grid.face_index[0][0, 1], np.arange(5, 10)) - assert np.allclose(grid.face_index[0][1, 1], np.arange(25, 30)) - # ... - assert np.allclose(grid.face_index[1][0, 0], np.arange(40, 45)) - assert np.allclose(grid.face_index[1][1, 0], np.arange(55, 60)) - assert np.allclose(grid.face_index[1][2, 0], np.arange(70, 75)) - assert np.allclose(grid.face_index[1][0, 1], np.arange(45, 50)) - # ... - assert np.allclose(grid.face_index[2][0, 0], np.arange(85, 89)) - assert np.allclose(grid.face_index[2][1, 0], np.arange(97, 101)) - assert np.allclose(grid.face_index[2][2, 0], np.arange(109, 113)) - assert np.allclose(grid.face_index[2][0, 1], np.arange(89, 93)) - # ... + # Check indexing of faces in 2d format + assert np.allclose(grid.face_index[0][:, 0, 0], np.arange(0, 2)) + assert np.allclose(grid.face_index[0][:, 1, 0], np.arange(2, 4)) + assert np.allclose(grid.face_index[0][:, 2, 0], np.arange(4, 6)) + assert np.allclose(grid.face_index[0][:, 0, 4], np.arange(32, 34)) + assert np.allclose(grid.face_index[0][:, 1, 4], np.arange(34, 36)) + assert np.allclose(grid.face_index[0][:, 2, 4], np.arange(36, 38)) + assert np.allclose(grid.face_index[1][:, 0, 0], np.arange(40, 43)) + assert np.allclose(grid.face_index[1][:, 1, 0], np.arange(43, 46)) + assert np.allclose(grid.face_index[1][:, 2, 0], np.arange(46, 49)) # Check identification of interior inner faces assert np.allclose( - grid.interior_faces[0], [6, 7, 8, 11, 12, 13, 26, 27, 28, 31, 32, 33] - ) - assert np.allclose(grid.interior_faces[1], [46, 47, 48, 61, 62, 63, 76, 77, 78]) - assert np.allclose(grid.interior_faces[2], [90, 91, 102, 103, 114, 115, 126, 127]) - - # Check identification of exterior inner faces - assert np.allclose( - grid.exterior_faces[0], + grid.interior_faces[0], [ - 0, - 1, - 2, - 3, - 4, - 5, - 9, 10, - 14, - 15, - 16, - 17, + 11, + 12, + 13, 18, 19, 20, 21, - 22, - 23, - 24, - 25, + 26, + 27, + 28, 29, - 30, - 34, - 35, - 36, - 37, - 38, - 39, - ], - ) - assert np.allclose( - grid.exterior_faces[1], - [ - 40, - 41, - 42, - 43, - 44, - 45, - 49, - 50, - 51, - 52, - 53, - 54, - 55, - 56, - 57, - 58, - 59, - 60, - 64, - 65, - 66, - 67, - 68, - 69, - 70, - 71, - 72, - 73, - 74, - 75, - 79, - 80, - 81, - 82, - 83, - 84, - ], - ) - assert np.allclose( - grid.exterior_faces[2], - [ - 85, - 86, - 87, - 88, - 89, - 92, - 93, - 94, - 95, - 96, - 97, - 98, - 99, - 100, - 101, - 104, - 105, - 106, - 107, - 108, - 109, - 110, - 111, - 112, - 113, - 116, - 117, - 118, - 119, - 120, - 121, - 122, - 123, - 124, - 125, - 128, - 129, - 130, - 131, - 132, ], ) + assert np.allclose(grid.interior_faces[1], [50, 53, 56, 59, 62, 65, 68, 71, 74]) + assert np.allclose(grid.interior_faces[2], [89, 92, 101, 104, 113, 116, 125, 128]) + + # Check identification of exterior inner faces + for d in range(grid.dim): + assert np.allclose( + np.sort(grid.exterior_faces[d]), + np.sort( + np.array( + list( + set(grid.faces[d].tolist()) + - set(grid.interior_faces[d].tolist()) + ) + ) + ), + ) + + +def test_grid_connectivity_3d(): + """Test connectivity for 3d tensor grids.""" + + # Fetch grid + grid = darsia.Grid(shape=(3, 4, 5), voxel_size=[0.5, 0.25, 2]) # Check connectivity: face to cells with positive orientation - # across 0-normal: - assert np.allclose(grid.connectivity[0], [0, 20]) - assert np.allclose(grid.connectivity[39], [39, 59]) - # across 1-normal: - assert np.allclose(grid.connectivity[40], [0, 5]) - assert np.allclose(grid.connectivity[84], [54, 59]) - # across 2-normal: - assert np.allclose(grid.connectivity[85], [0, 1]) - assert np.allclose(grid.connectivity[132], [58, 59]) + # Horizontal faces + assert np.allclose(grid.connectivity[0], [0, 1]) + assert np.allclose(grid.connectivity[1], [1, 2]) + assert np.allclose(grid.connectivity[2], [3, 4]) + assert np.allclose(grid.connectivity[3], [4, 5]) + assert np.allclose(grid.connectivity[4], [6, 7]) + assert np.allclose(grid.connectivity[5], [7, 8]) + assert np.allclose(grid.connectivity[6], [9, 10]) + assert np.allclose(grid.connectivity[7], [10, 11]) + # Vertical faces + assert np.allclose(grid.connectivity[40], [0, 3]) + assert np.allclose(grid.connectivity[41], [1, 4]) + assert np.allclose(grid.connectivity[42], [2, 5]) + assert np.allclose(grid.connectivity[43], [3, 6]) + assert np.allclose(grid.connectivity[44], [4, 7]) + assert np.allclose(grid.connectivity[45], [5, 8]) + assert np.allclose(grid.connectivity[46], [6, 9]) + assert np.allclose(grid.connectivity[47], [7, 10]) + assert np.allclose(grid.connectivity[48], [8, 11]) + # Table faces + assert np.allclose(grid.connectivity[85], [0, 12]) + assert np.allclose(grid.connectivity[86], [1, 13]) + assert np.allclose(grid.connectivity[87], [2, 14]) + assert np.allclose(grid.connectivity[88], [3, 15]) + assert np.allclose(grid.connectivity[89], [4, 16]) + assert np.allclose(grid.connectivity[90], [5, 17]) + assert np.allclose(grid.connectivity[91], [6, 18]) + assert np.allclose(grid.connectivity[92], [7, 19]) + assert np.allclose(grid.connectivity[93], [8, 20]) # Check reverse connectivity: cell to faces with positive orientation # For corner cells assert np.allclose(grid.reverse_connectivity[0, 0], [-1, 0]) assert np.allclose(grid.reverse_connectivity[1, 0], [-1, 40]) assert np.allclose(grid.reverse_connectivity[2, 0], [-1, 85]) - assert np.allclose(grid.reverse_connectivity[0, 19], [-1, 19]) - assert np.allclose(grid.reverse_connectivity[1, 19], [54, -1]) - assert np.allclose(grid.reverse_connectivity[2, 19], [100, -1]) + assert np.allclose(grid.reverse_connectivity[0, 9], [-1, 6]) + assert np.allclose(grid.reverse_connectivity[1, 9], [46, -1]) + assert np.allclose(grid.reverse_connectivity[2, 9], [-1, 94]) + assert np.allclose(grid.reverse_connectivity[0, 57], [-1, 38]) + assert np.allclose(grid.reverse_connectivity[1, 57], [82, -1]) + assert np.allclose(grid.reverse_connectivity[2, 57], [130, -1]) assert np.allclose(grid.reverse_connectivity[0, 59], [39, -1]) assert np.allclose(grid.reverse_connectivity[1, 59], [84, -1]) assert np.allclose(grid.reverse_connectivity[2, 59], [132, -1]) # For interior cells - assert np.allclose(grid.reverse_connectivity[0, 26], [6, 26]) - assert np.allclose(grid.reverse_connectivity[1, 26], [56, 61]) - assert np.allclose(grid.reverse_connectivity[2, 26], [105, 106]) + assert np.allclose(grid.reverse_connectivity[0, 16], [10, 11]) + assert np.allclose(grid.reverse_connectivity[1, 16], [50, 53]) + assert np.allclose(grid.reverse_connectivity[2, 16], [89, 101]) + + +@pytest.mark.parametrize("shape", [(3, 4), (3, 4, 5)]) +def test_compatibility(shape): + """Compatibility of connectivity, reverse connectivity and interior faces. + + Make sure that inner faces really only connect to two cells which have inner faces + in all directions again. + + """ + grid = darsia.Grid(shape=shape) + assert ( + np.count_nonzero( + np.concatenate( + [ + np.ravel( + grid.reverse_connectivity[ + d_perp, + np.ravel(grid.connectivity[grid.interior_faces[d]]), + ] + ) + for d in range(grid.dim) + for d_perp in np.delete(range(grid.dim), d) + ] + ) + == -1 + ) + == 0 + ) From 6d4154c77684bfb2c924853aef0acb3c2de5c9e3 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Tue, 19 Sep 2023 23:34:25 +0200 Subject: [PATCH 089/100] TST: Adapt divergence test for new indexing, and add 3d test --- tests/unit/test_fv.py | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/tests/unit/test_fv.py b/tests/unit/test_fv.py index 9fe8ea97..ebf9d684 100644 --- a/tests/unit/test_fv.py +++ b/tests/unit/test_fv.py @@ -17,17 +17,43 @@ def test_divergence_2d(): assert np.allclose(np.nonzero(divergence[0])[1], [0, 15]) assert np.allclose(divergence[0, np.array([0, 15])], [0.25, 0.5]) - assert np.allclose(np.nonzero(divergence[4])[1], [4, 18]) - assert np.allclose(divergence[4, np.array([4, 18])], [0.25, -0.5]) + assert np.allclose(np.nonzero(divergence[4])[1], [3, 15, 19]) + assert np.allclose(divergence[4, np.array([3, 15, 19])], [0.25, -0.5, 0.5]) - assert np.allclose(np.nonzero(divergence[15])[1], [10, 27]) - assert np.allclose(divergence[15, np.array([10, 27])], [-0.25, 0.5]) + assert np.allclose(np.nonzero(divergence[16])[1], [12, 27]) + assert np.allclose(divergence[16, np.array([12, 27])], [0.25, -0.5]) assert np.allclose(np.nonzero(divergence[19])[1], [14, 30]) assert np.allclose(divergence[19, np.array([14, 30])], [-0.25, -0.5]) # Check value for interior cell - assert np.allclose(np.nonzero(divergence[6])[1], [1, 6, 19, 20]) + assert np.allclose(np.nonzero(divergence[6])[1], [4, 5, 17, 21]) assert np.allclose( - divergence[6, np.array([1, 6, 19, 20])], [-0.25, 0.25, -0.5, 0.5] + divergence[6, np.array([4, 5, 17, 21])], [-0.25, 0.25, -0.5, 0.5] + ) + + +def test_divergence_3d(): + # Create divergence matrix + grid = darsia.Grid(shape=(3, 4, 5), voxel_size=[0.5, 0.25, 2]) + divergence = darsia.FVDivergence(grid).mat.todense() + + # Check shape + assert np.allclose(divergence.shape, (grid.num_cells, grid.num_faces)) + + # Check values in corner cells + assert np.allclose(np.nonzero(divergence[0])[1], [0, 40, 85]) + assert np.allclose(divergence[0, np.array([0, 40, 85])], [0.5, 1, 0.125]) + + assert np.allclose(np.nonzero(divergence[11])[1], [7, 48, 96]) + assert np.allclose(divergence[11, np.array([7, 48, 96])], [-0.5, -1, 0.125]) + + assert np.allclose(np.nonzero(divergence[59])[1], [39, 84, 132]) + assert np.allclose(divergence[59, np.array([39, 84, 132])], [-0.5, -1, -0.125]) + + # Check value for interior cell + assert np.allclose(np.nonzero(divergence[16])[1], [10, 11, 50, 53, 89, 101]) + assert np.allclose( + divergence[16, np.array([10, 11, 50, 53, 89, 101])], + [-0.5, 0.5, -1, 1, -0.125, 0.125], ) From 723fa4ddf9b6c7edb688e2749ede6862a81fad1e Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Tue, 19 Sep 2023 23:50:01 +0200 Subject: [PATCH 090/100] TST: Add test for mass matrices. --- tests/unit/test_fv.py | 60 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/tests/unit/test_fv.py b/tests/unit/test_fv.py index ebf9d684..8859a4c0 100644 --- a/tests/unit/test_fv.py +++ b/tests/unit/test_fv.py @@ -57,3 +57,63 @@ def test_divergence_3d(): divergence[16, np.array([10, 11, 50, 53, 89, 101])], [-0.5, 0.5, -1, 1, -0.125, 0.125], ) + + +def test_mass_2d(): + grid = darsia.Grid(shape=(4, 5), voxel_size=[0.5, 0.25]) + mass = darsia.FVMass(grid).mat.todense() + + # Check shape + assert np.allclose(mass.shape, (grid.num_cells, grid.num_cells)) + + # Check diagonal structure + assert np.linalg.norm(mass - np.diag(np.diag(mass))) < 1e-10 + + # Check diagonal values + assert len(np.unique(np.diag(mass))) == 1 + assert np.isclose(mass[0, 0], 0.125) + + +def test_mass_3d(): + grid = darsia.Grid(shape=(3, 4, 5), voxel_size=[0.5, 0.25, 2]) + mass = darsia.FVMass(grid).mat.todense() + + # Check shape + assert np.allclose(mass.shape, (grid.num_cells, grid.num_cells)) + + # Check diagonal structure + assert np.linalg.norm(mass - np.diag(np.diag(mass))) < 1e-10 + + # Check diagonal values + assert len(np.unique(np.diag(mass))) == 1 + assert np.isclose(mass[0, 0], 0.25) + + +def test_mass_face_2d(): + grid = darsia.Grid(shape=(4, 5), voxel_size=[0.5, 0.25]) + mass = darsia.FVMass(grid, mode="faces", lumping=True).mat.todense() + + # Check shape + assert np.allclose(mass.shape, (grid.num_faces, grid.num_faces)) + + # Check diagonal structure + assert np.linalg.norm(mass - np.diag(np.diag(mass))) < 1e-10 + + # Check diagonal values + assert len(np.unique(np.diag(mass))) == 1 + assert np.isclose(mass[0, 0], 0.5 * 0.125) + + +def test_mass_face_3d(): + grid = darsia.Grid(shape=(3, 4, 5), voxel_size=[0.5, 0.25, 2]) + mass = darsia.FVMass(grid, mode="faces", lumping=True).mat.todense() + + # Check shape + assert np.allclose(mass.shape, (grid.num_faces, grid.num_faces)) + + # Check diagonal structure + assert np.linalg.norm(mass - np.diag(np.diag(mass))) < 1e-10 + + # Check diagonal values + assert len(np.unique(np.diag(mass))) == 1 + assert np.isclose(mass[0, 0], 0.5 * 0.25) From 0ca6534e14b8e12d4c2ecf88b87f81405c9a46ac Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Wed, 20 Sep 2023 00:21:41 +0200 Subject: [PATCH 091/100] MAINT: adapt renaming and fix error (use concatenate) --- src/darsia/utils/fv.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/darsia/utils/fv.py b/src/darsia/utils/fv.py index 518c4b59..ed0e5b49 100644 --- a/src/darsia/utils/fv.py +++ b/src/darsia/utils/fv.py @@ -178,8 +178,8 @@ def __init__(self, grid: darsia.Grid) -> None: data = np.tile( 0.25 * np.ones( - 2 * np.array(grid.exterior_faces).size - + 4 * np.array(grid.interior_faces).size, + 2 * np.array(np.concatenate(grid.exterior_faces)).size + + 4 * np.array(np.concatenate(grid.interior_faces)).size, dtype=float, ), grid.dim - 1, @@ -207,7 +207,7 @@ def interleaf(a: np.ndarray, b: np.ndarray) -> np.ndarray: return np.ravel(np.vstack([a, b]).T) # Consider all close-by faces for each outer inner face which are orthogonal - pre_cols_outer = np.concatenate( + cols_outer = np.concatenate( [ np.ravel( grid.reverse_connectivity[ @@ -220,10 +220,10 @@ def interleaf(a: np.ndarray, b: np.ndarray) -> np.ndarray: ] ) # Clean up - remove true exterior faces - pre_cols_outer = pre_cols_outer[pre_cols_outer != -1] + cols_outer = cols_outer[cols_outer != -1] # Same for interior inner faces, - pre_cols_inner = np.concatenate( + cols_inner = np.concatenate( [ np.ravel( grid.reverse_connectivity[ @@ -235,11 +235,11 @@ def interleaf(a: np.ndarray, b: np.ndarray) -> np.ndarray: for d_perp in np.delete(range(grid.dim), d) ] ) - assert np.count_nonzero(pre_cols_inner == -1) == 0 + assert np.count_nonzero(cols_inner == -1) == 0 # Collect all rows and columns rows = np.concatenate((rows_outer, rows_inner)) - cols = np.concatenate((pre_cols_outer, pre_cols_inner)) + cols = np.concatenate((cols_outer, cols_inner)) # Construct and cache the sparse projection matrix self.mat = sps.csc_matrix( @@ -277,24 +277,24 @@ def face_to_cell(grid: darsia.Grid, flat_flux: np.ndarray) -> np.ndarray: if grid.dim >= 1: cell_flux[:-1, ..., 0] += 0.5 * flat_flux[grid.faces[0]].reshape( - grid.inner_faces_shape[0] + grid.faces_shape[0] ) cell_flux[1:, ..., 0] += 0.5 * flat_flux[grid.faces[0]].reshape( - grid.inner_faces_shape[0] + grid.faces_shape[0] ) if grid.dim >= 2: cell_flux[:, :-1, ..., 1] += 0.5 * flat_flux[grid.faces[1]].reshape( - grid.inner_faces_shape[1] + grid.faces_shape[1] ) cell_flux[:, 1:, ..., 1] += 0.5 * flat_flux[grid.faces[1]].reshape( - grid.inner_faces_shape[1] + grid.faces_shape[1] ) if grid.dim >= 3: cell_flux[:, :, :-1, ..., 2] += 0.5 * flat_flux[grid.faces[2]].reshape( - grid.inner_faces_shape[2] + grid.faces_shape[2] ) cell_flux[:, :, 1:, ..., 2] += 0.5 * flat_flux[grid.faces[2]].reshape( - grid.inner_faces_shape[2] + grid.faces_shape[2] ) if grid.dim > 3: raise NotImplementedError(f"Dimension {grid.dim} not supported.") From e82a308225b8f62da124b7ea0d66257231176dc3 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Wed, 20 Sep 2023 00:58:43 +0200 Subject: [PATCH 092/100] BUG: Apply correct ordering in reshaping --- src/darsia/utils/fv.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/darsia/utils/fv.py b/src/darsia/utils/fv.py index ed0e5b49..57616763 100644 --- a/src/darsia/utils/fv.py +++ b/src/darsia/utils/fv.py @@ -203,9 +203,6 @@ def __init__(self, grid: darsia.Grid) -> None: # faces with normal direction and all oriented in the same direction. Need to # exclude true exterior faces. - def interleaf(a: np.ndarray, b: np.ndarray) -> np.ndarray: - return np.ravel(np.vstack([a, b]).T) - # Consider all close-by faces for each outer inner face which are orthogonal cols_outer = np.concatenate( [ @@ -277,24 +274,24 @@ def face_to_cell(grid: darsia.Grid, flat_flux: np.ndarray) -> np.ndarray: if grid.dim >= 1: cell_flux[:-1, ..., 0] += 0.5 * flat_flux[grid.faces[0]].reshape( - grid.faces_shape[0] + grid.faces_shape[0], order="F" ) cell_flux[1:, ..., 0] += 0.5 * flat_flux[grid.faces[0]].reshape( - grid.faces_shape[0] + grid.faces_shape[0], order="F" ) if grid.dim >= 2: cell_flux[:, :-1, ..., 1] += 0.5 * flat_flux[grid.faces[1]].reshape( - grid.faces_shape[1] + grid.faces_shape[1], order="F" ) cell_flux[:, 1:, ..., 1] += 0.5 * flat_flux[grid.faces[1]].reshape( - grid.faces_shape[1] + grid.faces_shape[1], order="F" ) if grid.dim >= 3: cell_flux[:, :, :-1, ..., 2] += 0.5 * flat_flux[grid.faces[2]].reshape( - grid.faces_shape[2] + grid.faces_shape[2], order="F" ) cell_flux[:, :, 1:, ..., 2] += 0.5 * flat_flux[grid.faces[2]].reshape( - grid.faces_shape[2] + grid.faces_shape[2], order="F" ) if grid.dim > 3: raise NotImplementedError(f"Dimension {grid.dim} not supported.") From a1330576e8f7a5fb6a0e45b53f7f8b8b608e038e Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Wed, 20 Sep 2023 00:58:58 +0200 Subject: [PATCH 093/100] TST: Add many more fv tests. --- tests/unit/test_fv.py | 74 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/tests/unit/test_fv.py b/tests/unit/test_fv.py index 8859a4c0..2d454677 100644 --- a/tests/unit/test_fv.py +++ b/tests/unit/test_fv.py @@ -117,3 +117,77 @@ def test_mass_face_3d(): # Check diagonal values assert len(np.unique(np.diag(mass))) == 1 assert np.isclose(mass[0, 0], 0.5 * 0.25) + + +def test_tangential_reconstruction_2d_1(): + grid = darsia.Grid(shape=(2, 3), voxel_size=[0.5, 0.25]) + tangential_reconstruction = darsia.FVTangentialReconstruction(grid).mat.todense() + + # Check shape + assert np.allclose( + tangential_reconstruction.shape, (grid.num_faces, grid.num_faces) + ) + + # Check values - first for exterior faces + assert np.allclose(tangential_reconstruction[0], [0, 0, 0, 0.25, 0.25, 0, 0]) + assert np.allclose(tangential_reconstruction[4], [0.25, 0.25, 0, 0, 0, 0, 0]) + + # Check values - then for interior faces + assert np.allclose(tangential_reconstruction[1], [0, 0, 0, 0.25, 0.25, 0.25, 0.25]) + + +def test_tangential_reconstruction_2d_2(): + grid = darsia.Grid(shape=(3, 4), voxel_size=[0.5, 0.25]) + tangential_reconstruction = darsia.FVTangentialReconstruction(grid).mat.todense() + + # Check shape + assert np.allclose( + tangential_reconstruction.shape, (grid.num_faces, grid.num_faces) + ) + + # Check values - first for exterior faces + assert np.allclose(np.nonzero(tangential_reconstruction[0])[1], [8, 9]) + assert np.allclose(np.nonzero(tangential_reconstruction[7])[1], [15, 16]) + + # Check values - then for interior faces + assert np.allclose(np.nonzero(tangential_reconstruction[2])[1], [8, 9, 11, 12]) + assert np.allclose(np.nonzero(tangential_reconstruction[15])[1], [4, 5, 6, 7]) + + # Apply once and prove values + tangential_reconstruction_sparse = darsia.FVTangentialReconstruction(grid).mat + normal_flux = np.arange(grid.num_faces) + tangential_flux = tangential_reconstruction_sparse.dot(normal_flux) + assert np.allclose( + tangential_flux[np.array([0, 1, 4, 8, 12, 16])], [4.25, 4.75, 13, 0.5, 3.5, 3] + ) + + +def test_face_to_cell_2d(): + grid = darsia.Grid(shape=(3, 4), voxel_size=[0.5, 0.25]) + num_faces = grid.num_faces + flat_flux = np.arange(num_faces) + cell_flux = darsia.face_to_cell(grid, flat_flux) + + # Check shape + assert np.allclose(cell_flux.shape, (*grid.shape, grid.dim)) + + # Check values + print(cell_flux) + assert np.allclose(cell_flux[0, 0], [0, 4]) + assert np.allclose(cell_flux[2, 3], [3.5, 8]) + assert np.allclose(cell_flux[1, 1], [2.5, 10.5]) + + +def test_face_to_cell_3d(): + grid = darsia.Grid(shape=(3, 4, 5), voxel_size=[0.5, 0.25, 2]) + num_faces = grid.num_faces + flat_flux = np.arange(num_faces) + cell_flux = darsia.face_to_cell(grid, flat_flux) + + # Check shape + assert np.allclose(cell_flux.shape, (*grid.shape, grid.dim)) + + # Check values + assert np.allclose(cell_flux[0, 0, 0], [0, 20, 42.5]) + assert np.allclose(cell_flux[2, 3, 4], [19.5, 42, 66]) + assert np.allclose(cell_flux[1, 1, 1], [10.5, 51.5, 95]) From 4f111099333cbb11afe61d038f508a7e511f9516 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Thu, 21 Sep 2023 12:34:07 +0200 Subject: [PATCH 094/100] ENH: Significanlty simplify code for tangential reconstruction and add full reconstruction. --- src/darsia/utils/fv.py | 153 +++++++++++++++++++++++------------------ 1 file changed, 87 insertions(+), 66 deletions(-) diff --git a/src/darsia/utils/fv.py b/src/darsia/utils/fv.py index 57616763..ca4b74ae 100644 --- a/src/darsia/utils/fv.py +++ b/src/darsia/utils/fv.py @@ -147,7 +147,7 @@ def __init__( self.mat = mass_matrix -class FVTangentialReconstruction: +class FVTangentialFaceReconstruction: """Projection of normal fluxes on grid onto tangential components. The tangential components are defined as the components of the fluxes that are @@ -169,83 +169,104 @@ def __init__(self, grid: darsia.Grid) -> None: """ # Operator for averaging fluxes on orthogonal, neighboring faces - shape = ((grid.dim - 1) * grid.num_faces, grid.num_faces) + shape = (grid.num_faces, grid.num_faces) # Each interior inner face has four neighboring faces with normal direction # and all oriented in the same direction. In three dimensions, two such normal # directions exist. For outer inner faces, there are only two such neighboring # faces. - data = np.tile( - 0.25 - * np.ones( - 2 * np.array(np.concatenate(grid.exterior_faces)).size - + 4 * np.array(np.concatenate(grid.interior_faces)).size, - dtype=float, - ), - grid.dim - 1, - ) - - # The rows correspond to the faces for which the tangential fluxes are - # determined times the component of the tangential fluxes. - rows_outer = np.concatenate( - [np.repeat(grid.exterior_faces[d], 2) for d in range(grid.dim)] - ) + data = 0.25 * np.ones(4 * grid.num_faces, dtype=float) - rows_inner = np.concatenate( - [np.repeat(grid.interior_faces[d], 4) for d in range(grid.dim)] - ) + # The rows correspond to the faces for which the tangential fluxes (later to be + # tiled for addressing the right amount of vectorial components). + rows = np.concatenate([np.repeat(grid.faces[d], 4) for d in range(grid.dim)]) # The columns correspond to the (orthogonal) faces contributing to the average # of the tangential fluxes. The main idea is for each face to follow the - # connectivity. First, we consider the outer inner faces. For each face, we + # connectivity. + cols = [ + np.concatenate( + [ + np.ravel( + grid.reverse_connectivity[ + d_perp, + np.ravel(grid.connectivity[grid.faces[d]]), + ] + ) + for d in range(grid.dim) + for d_perp in [np.delete(range(grid.dim), d)[i]] + ] + ) + for i in range(grid.dim - 1) + ] + + # Construct and cache the sparse projection matrix, need to extract those faces + # which are not inner faces. + self.mat = [ + sps.csc_matrix( + ( + data[col != -1], + (rows[col != -1], col[col != -1]), + ), + shape=shape, + ) + for col in cols + ] - # Consider outer inner faces. For each face, we consider the two neighboring - # faces with normal direction and all oriented in the same direction. Need to - # exclude true exterior faces. + # Cache some informatio + self.num_tangential_directions = grid.dim - 1 + self.grid = grid - # Consider all close-by faces for each outer inner face which are orthogonal - cols_outer = np.concatenate( - [ - np.ravel( - grid.reverse_connectivity[ - d_perp, - np.ravel(grid.connectivity[grid.exterior_faces[d]]), - ] - ) - for d in range(grid.dim) - for d_perp in np.delete(range(grid.dim), d) - ] - ) - # Clean up - remove true exterior faces - cols_outer = cols_outer[cols_outer != -1] + def __call__(self, normal_flux: np.ndarray, concatenate: bool = True) -> np.ndarray: + """Apply the operator to the normal fluxes. - # Same for interior inner faces, - cols_inner = np.concatenate( - [ - np.ravel( - grid.reverse_connectivity[ - d_perp, - np.ravel(grid.connectivity[grid.interior_faces[d]]), - ] - ) - for d in range(grid.dim) - for d_perp in np.delete(range(grid.dim), d) - ] - ) - assert np.count_nonzero(cols_inner == -1) == 0 - - # Collect all rows and columns - rows = np.concatenate((rows_outer, rows_inner)) - cols = np.concatenate((cols_outer, cols_inner)) - - # Construct and cache the sparse projection matrix - self.mat = sps.csc_matrix( - ( - data, - (rows, cols), - ), - shape=shape, - ) + Args: + normal_flux (np.ndarray): normal fluxes + concatenate (bool, optional): whether to concatenate the tangential fluxes + + Returns: + np.ndarray: tangential fluxes + + """ + # Apply the operator to the normal fluxes + tangential_flux = [ + self.mat[d].dot(normal_flux) for d in range(self.num_tangential_directions) + ] + if concatenate: + tangential_flux = np.concatenate(tangential_flux, axis=0) + + return tangential_flux + + +class FVFullFaceReconstruction: + def __init__(self, grid: darsia.Grid) -> None: + self.grid = grid + self.tangential_reconstruction = FVTangentialFaceReconstruction(grid) + + def __call__(self, normal_flux: np.ndarray) -> np.ndarray: + """Reconstruct the full fluxes from the normal and tangential fluxes. + + Args: + normal_flux (np.ndarray): normal fluxes + + Returns: + np.ndarray: full fluxes + + """ + # Apply the operator to the normal fluxes + tangential_fluxes = self.tangential_reconstruction(normal_flux, False) + + # Reconstruct the full fluxes + dim = self.grid.dim + full_flux = np.zeros((self.grid.num_faces, dim), dtype=float) + for d in range(dim): + full_flux[self.grid.faces[d], d] = normal_flux[self.grid.faces[d]] + for i, d_perp in enumerate(np.delete(range(dim), d)): + full_flux[self.grid.faces[d], d_perp] = tangential_fluxes[i][ + self.grid.faces[d] + ] + + return full_flux # ! ---- Finite volume projection operators ---- From 90ff14810fc7f937e031712f989d2bd908254820 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Thu, 21 Sep 2023 12:34:37 +0200 Subject: [PATCH 095/100] TST: Cover tangential and full face reconstruction. --- tests/unit/test_fv.py | 103 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 94 insertions(+), 9 deletions(-) diff --git a/tests/unit/test_fv.py b/tests/unit/test_fv.py index 2d454677..3f5f8e29 100644 --- a/tests/unit/test_fv.py +++ b/tests/unit/test_fv.py @@ -121,7 +121,9 @@ def test_mass_face_3d(): def test_tangential_reconstruction_2d_1(): grid = darsia.Grid(shape=(2, 3), voxel_size=[0.5, 0.25]) - tangential_reconstruction = darsia.FVTangentialReconstruction(grid).mat.todense() + tangential_reconstruction = ( + darsia.FVTangentialFaceReconstruction(grid).mat[0].todense() + ) # Check shape assert np.allclose( @@ -138,30 +140,113 @@ def test_tangential_reconstruction_2d_1(): def test_tangential_reconstruction_2d_2(): grid = darsia.Grid(shape=(3, 4), voxel_size=[0.5, 0.25]) - tangential_reconstruction = darsia.FVTangentialReconstruction(grid).mat.todense() + tangential_reconstruction_dense = ( + darsia.FVTangentialFaceReconstruction(grid).mat[0].todense() + ) # Check shape assert np.allclose( - tangential_reconstruction.shape, (grid.num_faces, grid.num_faces) + tangential_reconstruction_dense.shape, (grid.num_faces, grid.num_faces) ) # Check values - first for exterior faces - assert np.allclose(np.nonzero(tangential_reconstruction[0])[1], [8, 9]) - assert np.allclose(np.nonzero(tangential_reconstruction[7])[1], [15, 16]) + assert np.allclose(np.nonzero(tangential_reconstruction_dense[0])[1], [8, 9]) + assert np.allclose(np.nonzero(tangential_reconstruction_dense[7])[1], [15, 16]) # Check values - then for interior faces - assert np.allclose(np.nonzero(tangential_reconstruction[2])[1], [8, 9, 11, 12]) - assert np.allclose(np.nonzero(tangential_reconstruction[15])[1], [4, 5, 6, 7]) + assert np.allclose( + np.nonzero(tangential_reconstruction_dense[2])[1], [8, 9, 11, 12] + ) + assert np.allclose(np.nonzero(tangential_reconstruction_dense[15])[1], [4, 5, 6, 7]) # Apply once and prove values - tangential_reconstruction_sparse = darsia.FVTangentialReconstruction(grid).mat + tangential_reconstruction = darsia.FVTangentialFaceReconstruction(grid) normal_flux = np.arange(grid.num_faces) - tangential_flux = tangential_reconstruction_sparse.dot(normal_flux) + tangential_flux = tangential_reconstruction(normal_flux) assert np.allclose( tangential_flux[np.array([0, 1, 4, 8, 12, 16])], [4.25, 4.75, 13, 0.5, 3.5, 3] ) +def test_full_reconstruction_2d_2(): + grid = darsia.Grid(shape=(3, 4), voxel_size=[0.5, 0.25]) + + # Apply once and prove values + tangential_reconstruction = darsia.FVFullFaceReconstruction(grid) + normal_flux = np.arange(grid.num_faces) + full_flux = tangential_reconstruction(normal_flux) + + # Check shape + assert np.allclose(full_flux.shape, (grid.num_faces, 2)) + + # Check values + assert np.allclose(full_flux[0], [0, 4.25]) + assert np.allclose(full_flux[1], [1, 4.75]) + assert np.allclose(full_flux[4], [4, 13]) + assert np.allclose(full_flux[8], [0.5, 8]) + assert np.allclose(full_flux[12], [3.5, 12]) + assert np.allclose(full_flux[16], [3, 16]) + + +def test_tangential_reconstruction_3d(): + grid = darsia.Grid(shape=(3, 3, 3), voxel_size=[0.5, 0.25, 2]) + tangential_reconstruction = darsia.FVTangentialFaceReconstruction(grid) + + # Apply once and probe values + normal_flux = np.arange(grid.num_faces) + tangential_flux = tangential_reconstruction(normal_flux, concatenate=False) + + # Corner block + assert np.allclose( + [tangential_flux[0][0], tangential_flux[1][0]], [(18 + 19) / 4, (36 + 37) / 4] + ) + assert np.allclose( + [tangential_flux[0][18], tangential_flux[1][18]], [(0 + 2) / 4, (36 + 39) / 4] + ) + assert np.allclose( + [tangential_flux[0][36], tangential_flux[1][36]], [(0 + 6) / 4, (18 + 24) / 4] + ) + + # Center block + assert np.allclose( + [tangential_flux[0][8], tangential_flux[1][8]], + [(24 + 25 + 27 + 28) / 4, (39 + 40 + 48 + 49) / 4], + ) + assert np.allclose( + [tangential_flux[0][25], tangential_flux[1][25]], + [(6 + 7 + 8 + 9) / 4, (37 + 40 + 46 + 49) / 4], + ) + assert np.allclose( + [tangential_flux[0][40], tangential_flux[1][40]], + [(2 + 3 + 8 + 9) / 4, (19 + 22 + 25 + 28) / 4], + ) + + +def test_full_reconstruction_3d(): + grid = darsia.Grid(shape=(3, 3, 3), voxel_size=[0.5, 0.25, 2]) + full_reconstuction = darsia.FVFullFaceReconstruction(grid) + + # Apply once and probe values + normal_flux = np.arange(grid.num_faces) + full_flux = full_reconstuction(normal_flux) + + # Corner block + assert np.allclose(full_flux[0], [0, (18 + 19) / 4, (36 + 37) / 4]) + assert np.allclose(full_flux[18], [(0 + 2) / 4, 18, (36 + 39) / 4]) + assert np.allclose(full_flux[36], [(0 + 6) / 4, (18 + 24) / 4, 36]) + + # Center block + assert np.allclose( + full_flux[8], [8, (24 + 25 + 27 + 28) / 4, (39 + 40 + 48 + 49) / 4] + ) + assert np.allclose( + full_flux[25], [(6 + 7 + 8 + 9) / 4, 25, (37 + 40 + 46 + 49) / 4] + ) + assert np.allclose( + full_flux[40], [(2 + 3 + 8 + 9) / 4, (19 + 22 + 25 + 28) / 4, 40] + ) + + def test_face_to_cell_2d(): grid = darsia.Grid(shape=(3, 4), voxel_size=[0.5, 0.25]) num_faces = grid.num_faces From 631043260181e4b6f532c7edce47b25f8ab52ddc Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Thu, 21 Sep 2023 19:50:44 +0200 Subject: [PATCH 096/100] MAINT: Improve error message for compatibility check of coordinate systems. --- src/darsia/image/arithmetics.py | 3 +- src/darsia/image/coordinatesystem.py | 76 +++++++++++++++++++--------- src/darsia/measure/emd.py | 3 +- 3 files changed, 55 insertions(+), 27 deletions(-) diff --git a/src/darsia/image/arithmetics.py b/src/darsia/image/arithmetics.py index a2a38aa1..d2bc199f 100644 --- a/src/darsia/image/arithmetics.py +++ b/src/darsia/image/arithmetics.py @@ -32,9 +32,10 @@ def weight(img: darsia.Image, weight: Union[float, int, darsia.Image]) -> darsia weighted_img.img *= weight elif isinstance(weight, darsia.Image): - assert darsia.check_equal_coordinatesystems( + equal_coordinate_system, log = darsia.check_equal_coordinatesystems( img.coordinatesystem, weight.coordinatesystem, exclude_size=True ) + assert equal_coordinate_system, f"{log}" space_dim = img.space_dim assert len(weight.img.shape) == space_dim diff --git a/src/darsia/image/coordinatesystem.py b/src/darsia/image/coordinatesystem.py index bc5cd830..75e94428 100644 --- a/src/darsia/image/coordinatesystem.py +++ b/src/darsia/image/coordinatesystem.py @@ -225,7 +225,7 @@ def check_equal_coordinatesystems( coordinatesystem1: CoordinateSystem, coordinatesystem2: CoordinateSystem, exclude_size: bool = False, -) -> bool: +) -> tuple[bool, dict]: """Check whether two coordinate systems are equivalent, i.e., they share basic attributes. @@ -236,38 +236,64 @@ def check_equal_coordinatesystems( Returns: bool: True iff the two coordinate systems are equivalent. + dict: log of the failed checks. """ success = True - success = success and coordinatesystem1.indexing == coordinatesystem2.indexing - success = success and coordinatesystem1.dim == coordinatesystem2.dim + failure_log = [] + + if not (coordinatesystem1.indexing == coordinatesystem2.indexing): + failure_log.append("indexing") + + if not (coordinatesystem1.dim == coordinatesystem2.dim): + failure_log.append("dim") + if not exclude_size: - success = success and np.allclose( - coordinatesystem1.shape, coordinatesystem2.shape - ) - success = success and np.allclose( - coordinatesystem1.dimensions, coordinatesystem2.dimensions - ) - success = success and coordinatesystem1.axes == coordinatesystem2.axes + if not (np.allclose(coordinatesystem1.shape, coordinatesystem2.shape)): + failure_log.append("shape") + + if not (np.allclose(coordinatesystem1.dimensions, coordinatesystem2.dimensions)): + failure_log.append("dimensions") + + if not (coordinatesystem1.axes == coordinatesystem2.axes): + failure_log.append("axes") + if not exclude_size: + voxel_size_equal = True for axis in coordinatesystem1.axes: - success = ( - success + voxel_size_equal = ( + voxel_size_equal and coordinatesystem1.voxel_size[axis] == coordinatesystem2.voxel_size[axis] ) - success = success and np.allclose( - coordinatesystem1._coordinate_of_origin_voxel, - coordinatesystem2._coordinate_of_origin_voxel, - ) - success = success and np.allclose( - coordinatesystem1._coordinate_of_opposite_voxel, - coordinatesystem2._coordinate_of_opposite_voxel, - ) - if not exclude_size: - success = success and np.allclose( - coordinatesystem1._voxel_of_origin_coordinate, - coordinatesystem2._voxel_of_origin_coordinate, + if not voxel_size_equal: + failure_log.append("voxel_size") + + if not ( + np.allclose( + coordinatesystem1._coordinate_of_origin_voxel, + coordinatesystem2._coordinate_of_origin_voxel, + ) + ): + failure_log.append("coordinate_of_origin_voxel") + + if not ( + np.allclose( + coordinatesystem1._coordinate_of_opposite_voxel, + coordinatesystem2._coordinate_of_opposite_voxel, ) + ): + failure_log.append("coordinate_of_opposite_voxel") + + if not exclude_size: + if not ( + np.allclose( + coordinatesystem1._voxel_of_origin_coordinate, + coordinatesystem2._voxel_of_origin_coordinate, + ) + ): + failure_log.append("voxel_of_origin_coordinate") + + success = len(failure_log) == 0 - return success + return success, failure_log diff --git a/src/darsia/measure/emd.py b/src/darsia/measure/emd.py index 69987c46..0a69cb9c 100644 --- a/src/darsia/measure/emd.py +++ b/src/darsia/measure/emd.py @@ -120,9 +120,10 @@ def _compatibility_check( assert img_1.space_dim == 2 and img_2.space_dim == 2 # Check whether the coordinate system is compatible - assert darsia.check_equal_coordinatesystems( + equal_coordinate_system, log = darsia.check_equal_coordinatesystems( img_1.coordinatesystem, img_2.coordinatesystem ) + assert equal_coordinate_system, f"{log}" assert np.allclose(img_1.voxel_size, img_2.voxel_size) # Compatible distributions - comparing sums is sufficient since it is implicitly From 6603ce4de91175147747fbcf20eeae4b418909c6 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Thu, 21 Sep 2023 19:52:07 +0200 Subject: [PATCH 097/100] MAINT: Move 2d requirement from general compatibiliity check --- src/darsia/measure/emd.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/darsia/measure/emd.py b/src/darsia/measure/emd.py index 0a69cb9c..e751f984 100644 --- a/src/darsia/measure/emd.py +++ b/src/darsia/measure/emd.py @@ -46,6 +46,10 @@ def __call__( float or array: distance between img_1 and img_2. """ + # Two-dimensional + if not (img_1.space_dim == 2 and img_2.space_dim == 2): + raise NotImplementedError("EMD only implemented for 2d.") + # FIXME investigation required regarding resize preprocessing... # Preprocess images preprocessed_img_1 = self._preprocess(img_1) @@ -116,9 +120,6 @@ def _compatibility_check( # Series assert img_1.time_num == img_2.time_num - # Two-dimensional - assert img_1.space_dim == 2 and img_2.space_dim == 2 - # Check whether the coordinate system is compatible equal_coordinate_system, log = darsia.check_equal_coordinatesystems( img_1.coordinatesystem, img_2.coordinatesystem From e6f11b97bedb65f8a6043e6affc81e8d07eded35 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Thu, 21 Sep 2023 19:54:22 +0200 Subject: [PATCH 098/100] MAINT: Adapt use of reconstruction operator. --- src/darsia/measure/wasserstein.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/darsia/measure/wasserstein.py b/src/darsia/measure/wasserstein.py index 7905c4d1..2e1f4ac3 100644 --- a/src/darsia/measure/wasserstein.py +++ b/src/darsia/measure/wasserstein.py @@ -173,8 +173,8 @@ def _setup_discretization(self) -> None: self.mass_matrix_faces = darsia.FVMass(self.grid, "faces", lumping).mat """sps.csc_matrix: mass matrix on faces: flat fluxes -> flat fluxes""" - self.tangential_projection = darsia.FVTangentialReconstruction(self.grid).mat - """sps.csc_matrix: tangential reconstruction: flat fluxes -> flat fluxes""" + self.face_reconstruction = darsia.FVFullFaceReconstruction(self.grid) + """sps.csc_matrix: full face reconstruction: flat fluxes -> vector fluxes""" # Linear part of the Darcy operator with potential constraint. self.broken_darcy = sps.bmat( @@ -470,9 +470,9 @@ def vector_face_flux_norm(self, flat_flux: np.ndarray, mode: str) -> np.ndarray: elif mode == "face_arithmetic": # Define natural vector valued flux on faces (taking arithmetic averages # of continuous fluxes over cells evaluated at faces) - tangential_flux = self.tangential_projection.dot(flat_flux) - # Determine the l2 norm of the fluxes on the faces, add some regularization - flat_flux_norm = np.sqrt(flat_flux**2 + tangential_flux**2) + full_face_flux = self.face_reconstruction(flat_flux) + # Determine the l2 norm of the fluxes on the faces + flat_flux_norm = np.linalg.norm(full_face_flux, 2, axis=1) else: raise ValueError(f"Mode {mode} not supported.") @@ -1194,9 +1194,9 @@ def _shrink( elif mode == "face_arithmetic": # Define natural vector valued flux on faces (taking arithmetic averages # of continuous fluxes over cells evaluated at faces) - tangential_flux = self.tangential_projection.dot(flat_flux) + full_face_flux = self.face_reconstruction(flat_flux) # Determine the l2 norm of the fluxes on the faces, add some regularization - norm = np.sqrt(flat_flux**2 + tangential_flux**2) + norm = np.linalg.norm(full_face_flux, 2, axis=1) flat_scaling = np.maximum(norm - shrink_factor, 0) / ( norm + self.regularization ) From 6a90f042e558a5b9504f4c04ab074ba2f5cd9c41 Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Thu, 21 Sep 2023 19:55:25 +0200 Subject: [PATCH 099/100] TST: Add 3d wasserstein test --- tests/unit/test_wasserstein.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/unit/test_wasserstein.py b/tests/unit/test_wasserstein.py index 379ed821..7c2ccbe9 100644 --- a/tests/unit/test_wasserstein.py +++ b/tests/unit/test_wasserstein.py @@ -33,13 +33,14 @@ # ! ---- 3d version ---- # Coarse src image -src_square_3d = np.zeros((rows, cols, 1), dtype=float) +pages = 1 +src_square_3d = np.zeros((rows, cols, pages), dtype=float) src_square_3d[2:5, 2:5, 0] = 1 meta_3d = {"dimensions": [1, 1, 1], "dim": 3, "series": False, "scalar": True} src_image_3d = darsia.Image(src_square_3d, **meta_3d) # Coarse dst image -dst_squares_3d = np.zeros((rows, cols, 1), dtype=float) +dst_squares_3d = np.zeros((rows, cols, pages), dtype=float) dst_squares_3d[1:3, 1:2, 0] = 1 dst_squares_3d[4:7, 7:9, 0] = 1 dst_image_3d = darsia.Image(dst_squares_3d, **meta_3d) @@ -144,7 +145,7 @@ @pytest.mark.parametrize("a_key", range(len(accelerations))) @pytest.mark.parametrize("s_key", range(len(solvers))) -@pytest.mark.parametrize("dim", [2]) +@pytest.mark.parametrize("dim", [2, 3]) def test_newton(a_key, s_key, dim): """Test all combinations for Newton.""" options.update(newton_options) @@ -163,7 +164,7 @@ def test_newton(a_key, s_key, dim): @pytest.mark.parametrize("a_key", range(len(accelerations))) @pytest.mark.parametrize("s_key", range(len(solvers))) -@pytest.mark.parametrize("dim", [2]) +@pytest.mark.parametrize("dim", [2, 3]) def test_std_bregman(a_key, s_key, dim): """Test all combinations for std Bregman.""" options.update(bregman_std_options) @@ -182,7 +183,7 @@ def test_std_bregman(a_key, s_key, dim): @pytest.mark.parametrize("a_key", range(len(accelerations))) @pytest.mark.parametrize("s_key", range(len(solvers))) -@pytest.mark.parametrize("dim", [2]) +@pytest.mark.parametrize("dim", [2, 3]) def test_reordered_bregman(a_key, s_key, dim): """Test all combinations for reordered Bregman.""" options.update(bregman_reordered_options) @@ -201,7 +202,7 @@ def test_reordered_bregman(a_key, s_key, dim): @pytest.mark.parametrize("a_key", range(len(accelerations))) @pytest.mark.parametrize("s_key", range(len(solvers))) -@pytest.mark.parametrize("dim", [2]) +@pytest.mark.parametrize("dim", [2, 3]) def test_adaptive_bregman(a_key, s_key, dim): """Test all combinations for adaptive Bregman.""" options.update(bregman_adaptive_options) From 4ee5a3815f5fd5ed3c28d49a569b809330dd16bc Mon Sep 17 00:00:00 2001 From: Jakub Both Date: Thu, 21 Sep 2023 20:27:02 +0200 Subject: [PATCH 100/100] MAINT: Remove obsolete 3d wasserstein file. --- src/darsia/__init__.py | 2 - src/darsia/measure/wasserstein3d.py | 949 ---------------------------- 2 files changed, 951 deletions(-) delete mode 100644 src/darsia/measure/wasserstein3d.py diff --git a/src/darsia/__init__.py b/src/darsia/__init__.py index 74896c5b..e6ba1a2f 100644 --- a/src/darsia/__init__.py +++ b/src/darsia/__init__.py @@ -13,8 +13,6 @@ from darsia.measure.integration import * from darsia.measure.emd import * from darsia.measure.wasserstein import * - -# from darsia.measure.wasserstein3d import * from darsia.utils.box import * from darsia.utils.interpolation import * from darsia.utils.segmentation import * diff --git a/src/darsia/measure/wasserstein3d.py b/src/darsia/measure/wasserstein3d.py deleted file mode 100644 index 455c6474..00000000 --- a/src/darsia/measure/wasserstein3d.py +++ /dev/null @@ -1,949 +0,0 @@ -"""Wasserstein distance computed using variational methods. - -3d case: to migrate to general file after development. - -""" -from __future__ import annotations - -import time - -import numpy as np -import scipy.sparse as sps - -import darsia - - -class VariationalWassersteinDistance3d(darsia.EMD): - """Base class for setting up the variational Wasserstein distance. - - The variational Wasserstein distance is defined as the solution to the following - optimization problem (also called the Beckman problem): - inf ||u||_{L^1} s.t. div u = m_1 - m_2, u in H(div). - u is the flux, m_1 and m_2 are the mass distributions which are transported by u - from m_1 to m_2. - - Specialized classes implement the solution of the Beckman problem using different - methods. There are two main methods: - - Newton's method (:class:`WassersteinDistanceNewton`) - - Split Bregman method (:class:`WassersteinDistanceBregman`) - - """ - - def __init__( - self, - shape: tuple, - voxel_size: list, - dim: int, - options: dict = {}, - ) -> None: - """ - Args: - - shape (tuple): shape of the image - voxel_size (list): voxel size of the image - dim (int): dimension of the problem - options (dict): options for the solver - - num_iter (int): maximum number of iterations. Defaults to 100. - - tol (float): tolerance for the stopping criterion. Defaults to 1e-6. - - L (float): parameter for the Bregman iteration. Defaults to 1.0. - - regularization (float): regularization parameter for the Bregman - iteration. Defaults to 0.0. - - depth (int): depth of the Anderson acceleration. Defaults to 0. - - scaling (float): scaling of the fluxes in the plot. Defaults to 1.0. - - lumping (bool): lump the mass matrix. Defaults to True. - - """ - # TODO improve documentation for options - method dependent - # Cache geometrical infos - self.shape = shape - self.voxel_size = voxel_size - self.dim = dim - - assert dim == 3, "Currently only 3D images are supported." - - self.options = options - self.regularization = self.options.get("regularization", 0.0) - self.verbose = self.options.get("verbose", False) - - # Setup - self._setup() - - def _setup(self) -> None: - """Setup of fixed discretization""" - - # Define dimensions of the problem - dim_cells = self.shape - num_cells = np.prod(dim_cells) - numbering_cells = np.arange(num_cells, dtype=int).reshape(dim_cells) - - # Consider only inner faces - vertical_faces_shape = (self.shape[0], self.shape[1] - 1, self.shape[2]) - horizontal_faces_shape = (self.shape[0] - 1, self.shape[1], self.shape[2]) - level_faces_shape = (self.shape[0], self.shape[1], self.shape[2] - 1) - num_faces_axis = [ - np.prod(vertical_faces_shape), - np.prod(horizontal_faces_shape), - np.prod(level_faces_shape), - ] - num_faces = np.sum(num_faces_axis) - - # Define connectivity - connectivity = np.zeros((num_faces, 2), dtype=int) - connectivity[: num_faces_axis[0], 0] = np.ravel( - numbering_cells[:, :-1, :] - ) # left cells - connectivity[: num_faces_axis[0], 1] = np.ravel( - numbering_cells[:, 1:, :] - ) # right cells - connectivity[ - num_faces_axis[0] : num_faces_axis[0] + num_faces_axis[1], 0 - ] = np.ravel( - numbering_cells[:-1, :, :] - ) # top cells - connectivity[ - num_faces_axis[0] : num_faces_axis[0] + num_faces_axis[1], 1 - ] = np.ravel( - numbering_cells[1:, :, :] - ) # bottom cells - connectivity[num_faces_axis[0] + num_faces_axis[1] :, 0] = np.ravel( - numbering_cells[:, :, :-1] - ) # FIXME (name?) cells - connectivity[num_faces_axis[0] + num_faces_axis[1] :, 1] = np.ravel( - numbering_cells[:, :, 1:] - ) # FIXME (name?) cells - - # Define sparse divergence operator, integrated over elements: flat_fluxes -> flat_mass - div_data = np.concatenate( - ( - self.voxel_size[0] * np.ones(num_faces_axis[0], dtype=float), - self.voxel_size[1] * np.ones(num_faces_axis[1], dtype=float), - self.voxel_size[2] * np.ones(num_faces_axis[2], dtype=float), - -self.voxel_size[0] * np.ones(num_faces_axis[0], dtype=float), - -self.voxel_size[1] * np.ones(num_faces_axis[1], dtype=float), - -self.voxel_size[2] * np.ones(num_faces_axis[2], dtype=float), - ) - ) - div_row = np.concatenate( - ( - connectivity[: num_faces_axis[0], 0], - connectivity[ - num_faces_axis[0] : num_faces_axis[0] + num_faces_axis[1], 0 - ], - connectivity[num_faces_axis[0] + num_faces_axis[1] :, 0], - connectivity[: num_faces_axis[0], 1], - connectivity[ - num_faces_axis[0] : num_faces_axis[0] + num_faces_axis[1], 1 - ], - connectivity[num_faces_axis[0] + num_faces_axis[1] :, 1], - ) - ) - div_col = np.tile(np.arange(num_faces, dtype=int), 2) - self.div = sps.csc_matrix( - (div_data, (div_row, div_col)), shape=(num_cells, num_faces) - ) - - # Define sparse mass matrix on cells: flat_mass -> flat_mass - self.mass_matrix_cells = sps.diags( - np.prod(self.voxel_size) * np.ones(num_cells, dtype=float) - ) - - # Define sparse mass matrix on faces: flat_fluxes -> flat_fluxes - lumping = self.options.get("lumping", True) - if lumping: - self.mass_matrix_faces = sps.diags( - np.prod(self.voxel_size) * np.ones(num_faces, dtype=float) - ) - else: - # Define connectivity: cell to face (only for inner cells) - connectivity_cell_to_vertical_face = np.zeros((num_cells, 2), dtype=int) - connectivity_cell_to_vertical_face[ - np.ravel(numbering_cells[:, :-1, :]), 0 - ] = np.arange( - num_faces_axis[0] - ) # left face - connectivity_cell_to_vertical_face[ - np.ravel(numbering_cells[:, 1:, :]), 1 - ] = np.arange( - num_faces_axis[0] - ) # right face - connectivity_cell_to_horizontal_face = np.zeros((num_cells, 2), dtype=int) - connectivity_cell_to_horizontal_face[ - np.ravel(numbering_cells[:-1, :, :]), 0 - ] = np.arange( - num_faces_axis[0], num_faces_axis[0] + num_faces_axis[1] - ) # top face - connectivity_cell_to_horizontal_face[ - np.ravel(numbering_cells[1:, :, :]), 1 - ] = np.arange( - num_faces_axis[0], num_faces_axis[0] + num_faces_axis[1] - ) # bottom face - connectivity_cell_to_level_face = np.zeros((num_cells, 2), dtype=int) - connectivity_cell_to_level_face[ - np.ravel(numbering_cells[:, :, :-1]), 0 - ] = np.arange( - num_faces_axis[0] + num_faces_axis[1], - num_faces_axis[0] + num_faces_axis[1] + num_faces_axis[2], - ) # FIXME (name?) face - connectivity_cell_to_level_face[ - np.ravel(numbering_cells[:, :, 1:]), 1 - ] = np.arange( - num_faces_axis[0] + num_faces_axis[1], - num_faces_axis[0] + num_faces_axis[1] + num_faces_axis[2], - ) # FIXME (name?) face - - # Info about inner cells - inner_cells_with_vertical_faces = np.ravel(numbering_cells[:, 1:-1, :]) - inner_cells_with_horizontal_faces = np.ravel(numbering_cells[1:-1, :, :]) - inner_cells_with_level_faces = np.ravel(numbering_cells[:, :, 1:-1]) - num_inner_cells_with_vertical_faces = len(inner_cells_with_vertical_faces) - num_inner_cells_with_horizontal_faces = len( - inner_cells_with_horizontal_faces - ) - num_inner_cells_with_level_faces = len(inner_cells_with_level_faces) - - # Define true RT0 mass matrix on faces: flat_fluxes -> flat_fluxes - mass_matrix_faces_data = np.prod(self.voxel_size) * np.concatenate( - ( - 2 / 3 * np.ones(num_faces, dtype=float), # all faces - 1 - / 6 - * np.ones( - num_inner_cells_with_vertical_faces, dtype=float - ), # left faces - 1 - / 6 - * np.ones( - num_inner_cells_with_vertical_faces, dtype=float - ), # right faces - 1 - / 6 - * np.ones( - num_inner_cells_with_horizontal_faces, dtype=float - ), # top faces - 1 - / 6 - * np.ones( - num_inner_cells_with_horizontal_faces, dtype=float - ), # bottom faces - 1 - / 6 - * np.ones( - num_inner_cells_with_level_faces, dtype=float - ), # FIXME (name?) faces - 1 - / 6 - * np.ones( - num_inner_cells_with_level_faces, dtype=float - ), # FIXME (name?) faces - ) - ) - mass_matrix_faces_row = np.concatenate( - ( - np.arange(num_faces, dtype=int), - connectivity_cell_to_vertical_face[ - inner_cells_with_vertical_faces, 0 - ], - connectivity_cell_to_vertical_face[ - inner_cells_with_vertical_faces, 1 - ], - connectivity_cell_to_horizontal_face[ - inner_cells_with_horizontal_faces, 0 - ], - connectivity_cell_to_horizontal_face[ - inner_cells_with_horizontal_faces, 1 - ], - connectivity_cell_to_level_face[inner_cells_with_level_faces, 0], - connectivity_cell_to_level_face[inner_cells_with_level_faces, 1], - ) - ) - mass_matrix_faces_col = np.concatenate( - ( - np.arange(num_faces, dtype=int), - connectivity_cell_to_vertical_face[ - inner_cells_with_vertical_faces, 1 - ], - connectivity_cell_to_vertical_face[ - inner_cells_with_vertical_faces, 0 - ], - connectivity_cell_to_horizontal_face[ - inner_cells_with_horizontal_faces, 1 - ], - connectivity_cell_to_horizontal_face[ - inner_cells_with_horizontal_faces, 0 - ], - connectivity_cell_to_level_face[inner_cells_with_level_faces, 1], - connectivity_cell_to_level_face[inner_cells_with_level_faces, 0], - ) - ) - self.mass_matrix_faces = sps.csc_matrix( - ( - mass_matrix_faces_data, - (mass_matrix_faces_row, mass_matrix_faces_col), - ), - shape=(num_faces, num_faces), - ) - - # Utilities - depth = self.options.get("depth", 0) - self.anderson = ( - darsia.AndersonAcceleration(dimension=num_faces, depth=depth) - if depth > 0 - else None - ) - - # TODO needs to be defined for each problem separately - - # Define sparse embedding operator for fluxes into full discrete DOF space - self.flux_embedding = sps.csc_matrix( - ( - np.ones(num_faces, dtype=float), - (np.arange(num_faces), np.arange(num_faces)), - ), - shape=(num_faces + num_cells + 1, num_faces), - ) - - # Cache - self.num_faces = num_faces - self.num_cells = num_cells - self.dim_cells = dim_cells - self.numbering_cells = numbering_cells - self.num_faces_axis = num_faces_axis - self.vertical_faces_shape = vertical_faces_shape - self.horizontal_faces_shape = horizontal_faces_shape - self.level_faces_shape = level_faces_shape - - def _problem_specific_setup(self, mass_diff: np.ndarray) -> None: - """Resetup of fixed discretization""" - - # TODO can't we just fix some cell, e.g., [0,0]. Move then this to the above. - - # Fix index of dominating contribution in image differece - self.constrained_cell_flat_index = np.argmax(np.abs(mass_diff)) - self.pressure_constraint = sps.csc_matrix( - ( - np.ones(1, dtype=float), - (np.zeros(1, dtype=int), np.array([self.constrained_cell_flat_index])), - ), - shape=(1, self.num_cells), - dtype=float, - ) - - # Linear part of the operator. - self.broken_darcy = sps.bmat( - [ - [None, -self.div.T, None], - [self.div, None, -self.pressure_constraint.T], - [None, self.pressure_constraint, None], - ], - format="csc", - ) - - def split_solution( - self, solution: np.ndarray - ) -> tuple[np.ndarray, np.ndarray, float]: - """Split the solution into fluxes, pressure and lagrange multiplier. - - Args: - solution (np.ndarray): solution - - Returns: - tuple: fluxes, pressure, lagrange multiplier - - """ - # Split the solution - flat_flux = solution[: self.num_faces] - flat_pressure = solution[self.num_faces : self.num_faces + self.num_cells] - flat_lagrange_multiplier = solution[-1] - - return flat_flux, flat_pressure, flat_lagrange_multiplier - - # ! ---- Projections inbetween faces and cells ---- - - def cell_reconstruction(self, flat_flux: np.ndarray) -> np.ndarray: - """Reconstruct the fluxes on the cells from the fluxes on the faces. - - Args: - flat_flux (np.ndarray): flat fluxes (normal fluxes on the faces) - - Returns: - np.ndarray: cell-based vectorial fluxes - - """ - # TODO replace by sparse matrix multiplication - - # Reshape fluxes - use duality of faces and normals - horizontal_fluxes = flat_flux[: self.num_faces_axis[0]].reshape( - self.vertical_faces_shape - ) - vertical_fluxes = flat_flux[ - self.num_faces_axis[0] : self.num_faces_axis[0] + self.num_faces_axis[1] - ].reshape(self.horizontal_faces_shape) - level_fluxes = flat_flux[ - self.num_faces_axis[0] + self.num_faces_axis[1] : - ].reshape(self.level_faces_shape) - - # Determine a cell-based Raviart-Thomas reconstruction of the fluxes - cell_flux = np.zeros((*self.dim_cells, self.dim), dtype=float) - # Horizontal fluxes - cell_flux[:, :-1, :, 0] += 0.5 * horizontal_fluxes - cell_flux[:, 1:, :, 0] += 0.5 * horizontal_fluxes - # Vertical fluxes - cell_flux[:-1, :, :, 1] += 0.5 * vertical_fluxes - cell_flux[1:, :, :, 1] += 0.5 * vertical_fluxes - # Level fluxes - cell_flux[:, :, :-1, 2] += 0.5 * level_fluxes - cell_flux[:, :, 1:, 2] += 0.5 * level_fluxes - - return cell_flux - - def face_restriction(self, cell_flux: np.ndarray) -> np.ndarray: - """Restrict the fluxes on the cells to the faces. - - Args: - cell_flux (np.ndarray): cell-based fluxes - - Returns: - np.ndarray: face-based fluxes - - """ - # TODO replace by sparse matrix multiplication - - # Determine the fluxes on the faces - horizontal_fluxes = 0.5 * (cell_flux[:, :-1, 0] + cell_flux[:, 1:, 0]) - vertical_fluxes = 0.5 * (cell_flux[:-1, :, 1] + cell_flux[1:, :, 1]) - level_fluxes = 0.5 * (cell_flux[:, :, :-1, 2] + cell_flux[:, :, 1:, 2]) - - # Reshape the fluxes - flat_flux = np.concatenate( - [horizontal_fluxes.ravel(), vertical_fluxes.ravel(), level_fluxes.ravel()], - axis=0, - ) - - return flat_flux - - def face_restriction_scalar(self, cell_qty: np.ndarray) -> np.ndarray: - """Restrict the fluxes on the cells to the faces. - - Args: - cell_qty (np.ndarray): cell-based quantity - - Returns: - np.ndarray: face-based quantity - - """ - # Determine the fluxes on the faces - - horizontal_face_qty = 0.5 * (cell_qty[:, :-1, :] + cell_qty[:, 1:, :]) - vertical_face_qty = 0.5 * (cell_qty[:-1, :, :] + cell_qty[1:, :, :]) - level_face_qty = 0.5 * (cell_qty[:, :, :-1] + cell_qty[:, :, 1:]) - - # Reshape the fluxes - hardcoding the connectivity here - face_qty = np.concatenate( - [ - horizontal_face_qty.ravel(), - vertical_face_qty.ravel(), - level_face_qty.ravel(), - ] - ) - - return face_qty - - # ! ---- Effective quantities ---- - - def effective_mobility(self, flat_flux: np.ndarray) -> np.ndarray: - """Compute the effective mobility of the solution. - - Args: - flat_flux (np.ndarray): flat fluxes - - Returns: - np.ndarray: effective mobility - """ - # TODO Use improved quadrature? - cell_flux = self.cell_reconstruction(flat_flux) - return np.linalg.norm(cell_flux, 2, axis=-1) - - def l1_dissipation(self, solution: np.ndarray) -> float: - """Compute the l1 dissipation potential of the solution. - - Args: - solution (np.ndarray): solution - - Returns: - float: l1 dissipation potential - - """ - # TODO use improved quadrature? - flat_flux, _, _ = self.split_solution(solution) - cell_flux = self.cell_reconstruction(flat_flux) - return np.sum(np.prod(self.voxel_size) * np.linalg.norm(cell_flux, 2, axis=-1)) - - # ! ---- Main methods ---- - - def __call__( - self, - img_1: darsia.Image, - img_2: darsia.Image, - plot_solution: bool = False, - return_solution: bool = False, - ) -> float: - """L1 Wasserstein distance for two images with same mass. - - NOTE: Images need to comply with the setup of the object. - - Args: - img_1 (darsia.Image): image 1 - img_2 (darsia.Image): image 2 - plot_solution (bool): plot the solution. Defaults to False. - return_solution (bool): return the solution. Defaults to False. - - Returns: - float or array: distance between img_1 and img_2. - - """ - - # Start taking time - tic = time.time() - - # Compatibilty check - assert img_1.scalar and img_2.scalar - self._compatibility_check_3d(img_1, img_2) - - # Determine difference of distriutions and define corresponding rhs - mass_diff = img_1.img - img_2.img - flat_mass_diff = np.ravel(mass_diff) - self._problem_specific_setup(mass_diff) - - print("Finished: problem specific setup") - - # Main method - distance, solution, status = self._solve(flat_mass_diff) - - print("finished: solve") - - # Split the solution - flat_flux, flat_pressure, _ = self.split_solution(solution) - - print("finished: split") - - # Reshape the fluxes and pressure - flux = self.cell_reconstruction(flat_flux) - pressure = flat_pressure.reshape(self.dim_cells) - - print("finished: cell reconstruction") - - # Determine effective mobility - mobility = self.effective_mobility(flat_flux) - - print("finished: effective mobility") - - # Stop taking time - toc = time.time() - status["elapsed_time"] = toc - tic - - # TODO consider suitable visualization? - # # Plot the solution - # if plot_solution: - # self._plot_solution(mass_diff, flux, pressure, mobility) - - if return_solution: - return distance, flux, pressure, mobility, status - else: - return distance - - def _compatibility_check_3d( - self, - img_1: darsia.Image, - img_2: darsia.Image, - ) -> bool: - """ - Compatibility check. - - Args: - img_1 (Image): image 1 - img_2 (Image): image 2 - - Returns: - bool: flag whether images 1 and 2 can be compared. - - """ - # Scalar valued - assert img_1.scalar and img_2.scalar - - # Series - assert img_1.time_num == img_2.time_num - - # Two-dimensional - assert img_1.space_dim == 3 and img_2.space_dim == 3 - - # Check whether the coordinate system is compatible - assert darsia.check_equal_coordinatesystems( - img_1.coordinatesystem, img_2.coordinatesystem - ) - assert np.allclose(img_1.voxel_size, img_2.voxel_size) - - # Compatible distributions - comparing sums is sufficient since it is implicitly - # assumed that the coordinate systems are equivalent. Check each time step - # separately. - assert np.allclose(self._sum(img_1), self._sum(img_2)) - - def _plot_solution( - self, - mass_diff: np.ndarray, - flux: np.ndarray, - pressure: np.ndarray, - mobility: np.ndarray, - ) -> None: - assert False - - -class WassersteinDistanceNewton3d(VariationalWassersteinDistance3d): - """Class to determine the L1 EMD/Wasserstein distance solved with Newton's method.""" - - def residual(self, rhs: np.ndarray, solution: np.ndarray) -> np.ndarray: - """Compute the residual of the solution. - - Args: - rhs (np.ndarray): right hand side - solution (np.ndarray): solution - - Returns: - np.ndarray: residual - - """ - flat_flux, _, _ = self.split_solution(solution) - cell_flux = self.cell_reconstruction(flat_flux) - cell_flux_norm = np.maximum( - np.linalg.norm(cell_flux, 2, axis=-1), self.regularization - ) - cell_flux_normed = cell_flux / cell_flux_norm[..., None] - flat_flux_normed = self.face_restriction(cell_flux_normed) - return ( - rhs - - self.broken_darcy.dot(solution) - - self.flux_embedding.dot(self.mass_matrix_faces.dot(flat_flux_normed)) - ) - - def jacobian_lu(self, solution: np.ndarray) -> sps.linalg.splu: - """Compute the LU factorization of the jacobian of the solution. - - Args: - solution (np.ndarray): solution - - Returns: - sps.linalg.splu: LU factorization of the jacobian - - """ - flat_flux, _, _ = self.split_solution(solution) - cell_flux = self.cell_reconstruction(flat_flux) - self.regularization = self.options.get("regularization", 0.0) - cell_flux_norm = np.maximum( - np.linalg.norm(cell_flux, 2, axis=-1), self.regularization - ) - flat_flux_norm = self.face_restriction_scalar(cell_flux_norm) - approx_jacobian = sps.bmat( - [ - [ - sps.diags(np.maximum(self.L, 1.0 / flat_flux_norm), dtype=float) - * self.mass_matrix_faces, - -self.div.T, - None, - ], - [self.div, None, -self.pressure_constraint.T], - [None, self.pressure_constraint, None], - ], - format="csc", - ) - approx_jacobian_lu = sps.linalg.splu(approx_jacobian) - return approx_jacobian_lu - - def _solve(self, flat_mass_diff): - # Observation: AA can lead to less stagnation, more accurate results, and therefore - # better solutions to mu and u. Higher depth is better, but more expensive. - - # Solver parameters - num_iter = self.options.get("num_iter", 100) - tol = self.options.get("tol", 1e-6) - tol_distance = self.options.get("tol_distance", 1e-6) - self.L = self.options.get("L", 1.0) - - # Define right hand side - rhs = np.concatenate( - [ - np.zeros(self.num_faces, dtype=float), - self.mass_matrix_cells.dot(flat_mass_diff), - np.zeros(1, dtype=float), - ] - ) - - # Initialize solution - solution_i = np.zeros_like(rhs) - - # Newton iteration - for iter in range(num_iter): - # Keep track of old flux, and old distance - old_solution_i = solution_i.copy() - old_distance = self.l1_dissipation(solution_i) - - # Newton step - if iter == 0: - residual_i = ( - rhs.copy() - ) # Aim at Darcy-like initial guess after first iteration. - else: - residual_i = self.residual(rhs, solution_i) - jacobian_lu = self.jacobian_lu(solution_i) - update_i = jacobian_lu.solve(residual_i) - solution_i += update_i - - # Apply Anderson acceleration to flux contribution (the only nonlinear part). - if self.anderson is not None: - solution_i[: self.num_faces] = self.anderson( - solution_i[: self.num_faces], update_i[: self.num_faces], iter - ) - # TODO try for full solution - - # Update distance - new_distance = self.l1_dissipation(solution_i) - - # Compute the error: - # - residual - # - residual of mass conservation equation - # - increment - # - flux increment - error = [ - np.linalg.norm(residual_i, 2), - np.linalg.norm(residual_i[self.num_faces : -1], 2), - np.linalg.norm(solution_i - old_solution_i, 2), - np.linalg.norm((solution_i - old_solution_i)[: self.num_faces], 2), - ] - - if self.verbose: - print( - "Newton iteration", - iter, - new_distance, - old_distance - new_distance, - error[0], # residual - error[1], # mass conservation residual - error[2], # full increment - error[3], # flux increment - ) - - # Stopping criterion - # TODO include criterion build on staganation of the solution - # TODO include criterion on distance. - if ( - iter > 1 - and min([error[0], error[2]]) < tol - or abs(new_distance - old_distance) < tol_distance - ): - break - - # Define performance metric - status = { - "converged": iter < num_iter, - "number iterations": iter, - "distance": new_distance, - "residual": error[0], - "mass conservation residual": error[1], - "increment": error[2], - "flux increment": error[3], - "distance increment": abs(new_distance - old_distance), - } - - return new_distance, solution_i, status - - -class WassersteinDistanceBregman3d(VariationalWassersteinDistance3d): - def _problem_specific_setup(self, mass_diff: np.ndarray) -> None: - super()._problem_specific_setup(mass_diff) - self.L = self.options.get("L", 1.0) - l_scheme_mixed_darcy = sps.bmat( - [ - [self.L * self.mass_matrix_faces, -self.div.T, None], - [self.div, None, -self.pressure_constraint.T], - [None, self.pressure_constraint, None], - ], - format="csc", - ) - self.l_scheme_mixed_darcy_lu = sps.linalg.splu(l_scheme_mixed_darcy) - - def _solve(self, flat_mass_diff): - # Solver parameters - num_iter = self.options.get("num_iter", 100) - # tol = self.options.get("tol", 1e-6) # TODO make use of tol, or remove - self.L = self.options.get("L", 1.0) - - rhs = np.concatenate( - [ - np.zeros(self.num_faces, dtype=float), - self.mass_matrix_cells.dot(flat_mass_diff), - np.zeros(1, dtype=float), - ] - ) - - # Keep track of how often the distance increases. - num_neg_diff = 0 - - # Bregman iterations - solution_i = np.zeros_like(rhs) - for iter in range(num_iter): - old_distance = self.l1_dissipation(solution_i) - - # 1. Solve linear system with trust in current flux. - flat_flux_i, _, _ = self.split_solution(solution_i) - rhs_i = rhs.copy() - rhs_i[: self.num_faces] = self.L * self.mass_matrix_faces.dot(flat_flux_i) - intermediate_solution_i = self.l_scheme_mixed_darcy_lu.solve(rhs_i) - - # 2. Shrink step for vectorial fluxes. To comply with the RT0 setting, the - # shrinkage operation merely determines the scalar. We still aim at - # following along the direction provided by the vectorial fluxes. - intermediate_flat_flux_i, _, _ = self.split_solution( - intermediate_solution_i - ) - # new_flat_flux_i = np.sign(intermediate_flat_flux_i) * ( - # np.maximum(np.abs(intermediate_flat_flux_i) - 1.0 / self.L, 0.0) - # ) - cell_intermediate_flux_i = self.cell_reconstruction( - intermediate_flat_flux_i - ) - norm = np.linalg.norm(cell_intermediate_flux_i, 2, axis=-1) - cell_scaling = np.maximum(norm - 1 / self.L, 0) / ( - norm + self.regularization - ) - flat_scaling = self.face_restriction_scalar(cell_scaling) - new_flat_flux_i = flat_scaling * intermediate_flat_flux_i - - # Apply Anderson acceleration to flux contribution (the only nonlinear part). - if self.anderson is not None: - flux_inc = new_flat_flux_i - flat_flux_i - new_flat_flux_i = self.anderson(new_flat_flux_i, flux_inc, iter) - - # Measure error in terms of the increment of the flux - flux_diff = np.linalg.norm(new_flat_flux_i - flat_flux_i, 2) - - # Update flux solution - solution_i = intermediate_solution_i.copy() - solution_i[: self.num_faces] = new_flat_flux_i - - # Update distance - new_distance = self.l1_dissipation(solution_i) - - # Determine the error in the mass conservation equation - mass_conservation_residual = np.linalg.norm( - (rhs_i - self.broken_darcy.dot(solution_i))[self.num_faces : -1], 2 - ) - - # TODO include criterion build on staganation of the solution - # TODO include criterion on distance. - - # Print status - if self.verbose: - print( - "Bregman iteration", - iter, - new_distance, - old_distance - new_distance, - self.L, - flux_diff, - mass_conservation_residual, - ) - - ## Check stopping criterion # TODO. What is a good stopping criterion? - # if iter > 1 and (flux_diff < tol or mass_conservation_residual < tol: - # break - - # Keep track if the distance increases. - if new_distance > old_distance: - num_neg_diff += 1 - - # Increase L if stagnating of the distance increases too often. - # TODO restart anderson acceleration - update_l = self.options.get("update_l", True) - if update_l: - tol_distance = self.options.get("tol_distance", 1e-12) - max_iter_increase_diff = self.options.get("max_iter_increase_diff", 20) - l_factor = self.options.get("l_factor", 2) - if ( - abs(new_distance - old_distance) < tol_distance - or num_neg_diff > max_iter_increase_diff - ): - # Update L - self.L = self.L * l_factor - - # Update linear system - l_scheme_mixed_darcy = sps.bmat( - [ - [self.L * self.mass_matrix_faces, -self.div.T, None], - [self.div, None, -self.pressure_constraint.T], - [None, self.pressure_constraint, None], - ], - format="csc", - ) - self.l_scheme_mixed_darcy_lu = sps.linalg.splu(l_scheme_mixed_darcy) - - # Reset stagnation counter - num_neg_diff = 0 - - L_max = self.options.get("L_max", 1e8) - if self.L > L_max: - break - - # Define performance metric - status = { - "converged": iter < num_iter, - "number iterations": iter, - "distance": new_distance, - "residual mass conservation": mass_conservation_residual, - "flux increment": flux_diff, - "distance increment": abs(new_distance - old_distance), - } - - return new_distance, solution_i, status - - -# Unified access -def wasserstein_distance_3d( - mass_1: darsia.Image, - mass_2: darsia.Image, - method: str, - **kwargs, -): - """Unified access to Wasserstein distance computation between images with same mass. - - Args: - mass_1 (darsia.Image): image 1 - mass_2 (darsia.Image): image 2 - method (str): method to use ("newton", "bregman", or "cv2.emd") - **kwargs: additional arguments (only for "newton" and "bregman") - - options (dict): options for the method. - - plot_solution (bool): plot the solution. Defaults to False. - - return_solution (bool): return the solution. Defaults to False. - - """ - if method.lower() in ["newton", "bregman"]: - shape = mass_1.img.shape - voxel_size = mass_1.voxel_size - dim = mass_1.space_dim - plot_solution = kwargs.get("plot_solution", False) - return_solution = kwargs.get("return_solution", False) - options = kwargs.get("options", {}) - options["name"] = kwargs.get("name") - - if method.lower() == "newton": - w1 = WassersteinDistanceNewton3d(shape, voxel_size, dim, options) - elif method.lower() == "bregman": - w1 = WassersteinDistanceBregman3d(shape, voxel_size, dim, options) - return w1( - mass_1, mass_2, plot_solution=plot_solution, return_solution=return_solution - ) - - elif method.lower() == "cv2.emd": - preprocess = kwargs.get("preprocess") - w1 = darsia.EMD(preprocess) - return w1(mass_1, mass_2) - - else: - raise NotImplementedError(f"Method {method} not implemented.")