Skip to content

Commit

Permalink
Merge pull request #87 from Jammy2211/feature/tracer_contour
Browse files Browse the repository at this point in the history
Feature/tracer contour
  • Loading branch information
Jammy2211 authored Jan 15, 2024
2 parents e7d256c + 51d87a0 commit 0b79d16
Show file tree
Hide file tree
Showing 24 changed files with 464 additions and 35 deletions.
2 changes: 1 addition & 1 deletion autoarray/config/visualize/include.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ include_1d:
include_2d:
border: true # Include the border of the mask (all pixels on the outside of the mask) ?
grid: false # Include the data's 2D grid of (y,x) coordinates ?
mapper_image_plane_mesh_grid: true # For an Inversion, include the pixel centres computed in the image-plane / data frame?
mapper_image_plane_mesh_grid: false # For an Inversion, include the pixel centres computed in the image-plane / data frame?
mapper_source_plane_data_grid: false # For an Inversion, include the centres of the image-plane grid mapped to the source-plane / frame in source-plane figures?
mapper_source_plane_mesh_grid: false # For an Inversion, include the centres of the mesh pixels in the source-plane / source-plane?
mask: true # Include a mask ?
Expand Down
9 changes: 5 additions & 4 deletions autoarray/config/visualize/plots.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@ inversion: # Settings for plots of inversions (e
all_at_end_png: true # Plot all individual plots listed below as .png (even if False)?
all_at_end_fits: true # Plot all individual plots listed below as .fits (even if False)?
all_at_end_pdf: false # Plot all individual plots listed below as publication-quality .pdf (even if False)?
errors: false
reconstructed_image: false
reconstruction: false
regularization_weights: false
errors: false # Plot image of the errors of every mesh-pixel reconstructed value?
mesh_pixels_per_image_pixels : false # Plot the number of image-plane mesh pixels per masked data pixels?
reconstructed_image: false # Plot image of the reconstructed data (e.g. in the image-plane)?
reconstruction: false # Plot the reconstructed inversion (e.g. the pixelization's mesh in the source-plane)?
regularization_weights: false # Plot the effective regularization weight of every inversion mesh pixel?
interferometer: # Settings for plots of interferometer datasets (e.g. InterferometerPlotter).
amplitudes_vs_uv_distances: false
phases_vs_uv_distances: false
Expand Down
26 changes: 26 additions & 0 deletions autoarray/inversion/inversion/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ def __init__(
use_w_tilde_numpy: bool = False,
use_source_loop: bool = False,
use_linear_operators: bool = False,
image_mesh_min_mesh_pixels_per_pixel=None,
image_mesh_min_mesh_number: int = 5,
image_mesh_adapt_background_percent_threshold: float = None,
image_mesh_adapt_background_percent_check: float = 0.1,
tolerance: float = 1e-8,
maxiter: int = 250,
):
Expand Down Expand Up @@ -53,6 +57,19 @@ def __init__(
use_linear_operators
For an interferometer inversion, whether to use the linear operator solution to solve the linear system
or not (this input does nothing for dataset data).
image_mesh_min_mesh_pixels_per_pixel
If not None, the image-mesh must place this many mesh pixels per image pixels in the N highest weighted
regions of the adapt data, or an `InversionException` is raised. This can be used to force the image-mesh
to cluster large numbers of source pixels to the adapt-datas brightest regions.
image_mesh_min_mesh_number
The value N given above in the docstring for `image_mesh_min_mesh_pixels_per_pixel`, indicating how many
image pixels are checked for having a threshold number of mesh pixels.
image_mesh_adapt_background_percent_threshold
If not None, the image-mesh must place this percentage of mesh-pixels in the background regions of the
`adapt_data`, where the background is the `image_mesh_adapt_background_percent_check` masked data pixels
with the lowest values.
image_mesh_adapt_background_percent_check
The percentage of masked data pixels which are checked for the background criteria.
tolerance
For an interferometer inversion using the linear operators method, sets the tolerance of the solver
(this input does nothing for dataset data and other interferometer methods).
Expand All @@ -72,6 +89,15 @@ def __init__(
self._no_regularization_add_to_curvature_diag_value = (
no_regularization_add_to_curvature_diag_value
)
self.image_mesh_min_mesh_pixels_per_pixel = image_mesh_min_mesh_pixels_per_pixel
self.image_mesh_min_mesh_number = image_mesh_min_mesh_number
self.image_mesh_adapt_background_percent_threshold = (
image_mesh_adapt_background_percent_threshold
)
self.image_mesh_adapt_background_percent_check = (
image_mesh_adapt_background_percent_check
)

self.tolerance = tolerance
self.maxiter = maxiter
self.use_w_tilde_numpy = use_w_tilde_numpy
Expand Down
2 changes: 1 addition & 1 deletion autoarray/inversion/mock/mock_image_mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def __init__(self, image_plane_mesh_grid=None):
self.image_plane_mesh_grid = image_plane_mesh_grid

def image_plane_mesh_grid_from(
self, grid: Grid2D, adapt_data: Optional[np.ndarray]
self, grid: Grid2D, adapt_data: Optional[np.ndarray], settings=None
) -> Grid2DIrregular:
if adapt_data is not None and self.image_plane_mesh_grid is not None:
return adapt_data * self.image_plane_mesh_grid
Expand Down
154 changes: 153 additions & 1 deletion autoarray/inversion/pixelization/image_mesh/abstract.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
from typing import Optional

import copy
import numpy as np

from autoarray.structures.arrays.uniform_2d import Array2D
from autoarray.structures.grids.uniform_2d import Grid2D
from autoarray.structures.grids.irregular_2d import Grid2DIrregular

from autoarray.structures.grids import grid_2d_util

from autoarray import exc


class AbstractImageMesh:
def __init__(self):
Expand Down Expand Up @@ -45,6 +51,152 @@ def weight_map_from(self, adapt_data: np.ndarray):
return weight_map

def image_plane_mesh_grid_from(
self, grid: Grid2D, adapt_data: Optional[np.ndarray] = None
self, grid: Grid2D, adapt_data: Optional[np.ndarray] = None, settings=None
) -> Grid2DIrregular:
raise NotImplementedError

def mesh_pixels_per_image_pixels_from(
self, grid: Grid2D, mesh_grid: Grid2DIrregular
) -> Array2D:
"""
Returns an array containing the number of mesh pixels in every pixel of the data's mask.
For example, image-mesh adaption may be performed on a 3.0" circular mask of data. The high weight pixels
may have 3 or more mesh pixels per image pixel, whereas low weight regions may have zero pixels. The array
returned by this function gives the integer number of pixels in each data pixel.
Parameters
----------
grid
The masked (y,x) grid of the data coordinates, corresponding to the mask applied to the data. The number of
mesh pixels mapped inside each of this grid's image-pixels is returned.
mesh_grid
The image mesh-grid computed by the class which adapts to the data's mask. The number of image mesh pixels
that fall within each of the data's mask pixels is returned.
Returns
-------
An array containing the integer number of image-mesh pixels that fall without each of the data's mask.
"""

mesh_pixels_per_image_pixels = grid_2d_util.grid_pixels_in_mask_pixels_from(
grid=mesh_grid,
shape_native=grid.shape_native,
pixel_scales=grid.pixel_scales,
origin=grid.origin,
)

return Array2D(values=mesh_pixels_per_image_pixels, mask=grid.mask)

def check_mesh_pixels_per_image_pixels(self, grid, mesh_grid, settings):
"""
Checks the number of mesh pixels in every image pixel and raises an `InversionException` if there are fewer
mesh pixels inside a certain number of image-pixels than the input settings.
This allows a user to force a model-fit to use image-mesh's which cluster a large number of mesh pixels to
the brightest regions of the image data (E.g. the highst weighted regions).
The check works as follows:
1) Compute the 2D array of the number of mesh pixels in every masked data image pixel.
2) Find the number of mesh pixels in the N data pixels with the larger number of mesh pixels, where N is
given by `settings.image_mesh_min_mesh_number`. For example, if `settings.image_mesh_min_mesh_number=5` then
the number of mesh pixels in the 5 data pixels with the most data pixels is computed.
3) Compare the lowest value above to the value `settings.image_mesh_min_mesh_pixels_per_pixel`. If the value is
below this value, raise an `InversionException`.
Therefore, by settings `settings.image_mesh_min_mesh_pixels_per_pixel` to a value above 1 the code is forced
to adapt the image mesh enough to put many mesh pixels in the brightest image pixels.
Parameters
----------
grid
The masked (y,x) grid of the data coordinates, corresponding to the mask applied to the data. The number of
mesh pixels mapped inside each of this grid's image-pixels is returned.
mesh_grid
The image mesh-grid computed by the class which adapts to the data's mask. The number of image mesh pixels
that fall within each of the data's mask pixels is returned.
settings
The inversion settings, which have the criteria dictating if the image-mesh has clustered enough or if
an exception is raised.
"""

if settings is not None:
if settings.image_mesh_min_mesh_pixels_per_pixel is not None:
mesh_pixels_per_image_pixels = self.mesh_pixels_per_image_pixels_from(
grid=grid, mesh_grid=mesh_grid
)

indices_of_highest_values = np.argsort(mesh_pixels_per_image_pixels)[
-settings.image_mesh_min_mesh_number :
]
lowest_mesh_pixels = np.min(
mesh_pixels_per_image_pixels[indices_of_highest_values]
)

if lowest_mesh_pixels < settings.image_mesh_min_mesh_pixels_per_pixel:
raise exc.InversionException()

return mesh_grid

def check_adapt_background_pixels(self, grid, mesh_grid, adapt_data, settings):
"""
Checks the number of mesh pixels in the background of the image-mesh and raises an `InversionException` if
there are fewer mesh pixels in the background than the input settings.
This allows a user to force a model-fit to use image-mesh's which cluster a minimum number of mesh pixels to
the faintest regions of the image data (E.g. the lowest weighted regions). This prevents too few image-mesh
pixels being allocated to the background of the data.
The check works as follows:
1) Find all pixels in the background of the `adapt_data`, which are N pixels with the lowest values, where N is
a percentage given by `settings.image_mesh_adapt_background_percent_check`. If N is 50%, then the half of
pixels in `adapt_data` with the lowest values will be checked.
2) Sum the total number of mesh pixels in these background pixels, thereby estimating the number of mesh pixels
assigned to background pixels.
3) Compare this value to the total number of mesh pixels multiplied
by `settings.image_mesh_adapt_background_percent_threshold` and raise an `InversionException` if the number
of mesh pixels is below this value, meaning the background did not have sufficient mesh pixels in it.
Therefore, by setting `settings.image_mesh_adapt_background_percent_threshold` the code is forced
to adapt the image mesh in a way that places many mesh pixels in the background regions.
Parameters
----------
grid
The masked (y,x) grid of the data coordinates, corresponding to the mask applied to the data. The number of
mesh pixels mapped inside each of this grid's image-pixels is returned.
mesh_grid
The image mesh-grid computed by the class which adapts to the data's mask. The number of image mesh pixels
that fall within each of the data's mask pixels is returned.
adapt_data
A image which represents one or more components in the masked 2D data in the image-plane.
settings
The inversion settings, which have the criteria dictating if the image-mesh has clustered enough or if
an exception is raised.
"""
if settings is not None:
if settings.image_mesh_adapt_background_percent_threshold is not None:
pixels = mesh_grid.shape[0]

pixels_in_background = int(
grid.shape[0] * settings.image_mesh_adapt_background_percent_check
)

indices_of_lowest_values = np.argsort(adapt_data)[:pixels_in_background]
mask_background = np.zeros_like(adapt_data, dtype=bool)
mask_background[indices_of_lowest_values] = True

mesh_pixels_per_image_pixels = self.mesh_pixels_per_image_pixels_from(
grid=grid, mesh_grid=mesh_grid
)

mesh_pixels_in_background = sum(
mesh_pixels_per_image_pixels[mask_background]
)

if mesh_pixels_in_background < (
pixels * settings.image_mesh_adapt_background_percent_threshold
):
raise exc.InversionException()
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,6 @@ def weight_map_from(self, adapt_data: np.ndarray):
return weight_map

def image_plane_mesh_grid_from(
self, grid: Grid2D, adapt_data: Optional[np.ndarray] = None
self, grid: Grid2D, adapt_data: Optional[np.ndarray] = None, settings=None
) -> Grid2DIrregular:
raise NotImplementedError
18 changes: 16 additions & 2 deletions autoarray/inversion/pixelization/image_mesh/hilbert.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from autoarray.inversion.pixelization.image_mesh.abstract_weighted import (
AbstractImageMeshWeighted,
)
from autoarray.inversion.inversion.settings import SettingsInversion
from autoarray.structures.grids.irregular_2d import Grid2DIrregular

from autoarray import exc
Expand Down Expand Up @@ -245,7 +246,10 @@ def __init__(
)

def image_plane_mesh_grid_from(
self, grid: Grid2D, adapt_data: Optional[np.ndarray]
self,
grid: Grid2D,
adapt_data: Optional[np.ndarray],
settings: SettingsInversion = None,
) -> Grid2DIrregular:
"""
Returns an image mesh by running the Hilbert curve on the weight map.
Expand Down Expand Up @@ -296,4 +300,14 @@ def image_plane_mesh_grid_from(
gridy=grid_hb[:, 0],
)

return Grid2DIrregular(values=np.stack((drawn_y, drawn_x), axis=-1))
mesh_grid = Grid2DIrregular(values=np.stack((drawn_y, drawn_x), axis=-1))

self.check_mesh_pixels_per_image_pixels(
grid=grid, mesh_grid=mesh_grid, settings=settings
)

self.check_adapt_background_pixels(
grid=grid, mesh_grid=mesh_grid, adapt_data=adapt_data, settings=settings
)

return mesh_grid
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def __init__(
self.ratio = ratio

def image_plane_mesh_grid_from(
self, grid: Grid2D, adapt_data: Optional[np.ndarray]
self, grid: Grid2D, adapt_data: Optional[np.ndarray], settings=None
) -> Grid2DIrregular:
"""
Returns an image mesh by running the balanced Hilbert curve on the weight map.
Expand Down Expand Up @@ -135,7 +135,7 @@ def image_plane_mesh_grid_from(
drawn_y,
) = inverse_transform_sampling_interpolated(
probabilities=weight_map_background,
n_samples=(self.pixels - pixels) + 1,
n_samples=(self.pixels - pixels) + 1,
gridx=grid_hb[:, 1],
gridy=grid_hb[:, 0],
)
Expand Down
6 changes: 3 additions & 3 deletions autoarray/inversion/pixelization/image_mesh/kmeans.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def __init__(
)

def image_plane_mesh_grid_from(
self, grid: Grid2D, adapt_data: Optional[np.ndarray]
self, grid: Grid2D, adapt_data: Optional[np.ndarray], settings=None
) -> Grid2DIrregular:
"""
Returns an image mesh by running a KMeans clustering algorithm on the weight map.
Expand All @@ -72,7 +72,6 @@ def image_plane_mesh_grid_from(
"""

if self.pixels > grid.shape[0]:

print(
"""
The number of pixels passed to the KMeans object exceeds the number of image-pixels in the mask of
Expand All @@ -87,7 +86,8 @@ def image_plane_mesh_grid_from(
For adaptive fitting, the KMeans object has been superseeded by the Hilbert object, which does not
have this limitation and performs better in general. You should therefore consider using the Hilbert
object instead.
""")
"""
)

sys.exit()

Expand Down
2 changes: 1 addition & 1 deletion autoarray/inversion/pixelization/image_mesh/overlay.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ def __init__(self, shape=(3, 3)):
self.shape = (int(shape[0]), int(shape[1]))

def image_plane_mesh_grid_from(
self, grid: Grid2D, adapt_data: Optional[np.ndarray] = None
self, grid: Grid2D, adapt_data: Optional[np.ndarray] = None, settings=None
) -> Grid2DIrregular:
"""
Returns an image-mesh by overlaying a uniform grid of (y,x) coordinates over the masked image that the
Expand Down
21 changes: 21 additions & 0 deletions autoarray/inversion/pixelization/mappers/mapper_grids.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
if TYPE_CHECKING:
from autoarray import Preloads

from autoarray.structures.arrays.uniform_2d import Array2D
from autoarray.structures.grids.uniform_2d import Grid2D
from autoarray.structures.grids.irregular_2d import Grid2DIrregular
from autoarray.structures.mesh.abstract_2d import Abstract2DMesh

from autoarray.structures.grids import grid_2d_util


class MapperGrids:
def __init__(
Expand Down Expand Up @@ -69,3 +72,21 @@ def __init__(
self.adapt_data = adapt_data
self.preloads = preloads or Preloads()
self.run_time_dict = run_time_dict

@property
def image_plane_data_grid(self):
return self.source_plane_data_grid.derive_grid.unmasked

@property
def mesh_pixels_per_image_pixels(self):
mesh_pixels_per_image_pixels = grid_2d_util.grid_pixels_in_mask_pixels_from(
grid=self.image_plane_mesh_grid,
shape_native=self.source_plane_data_grid.mask.shape_native,
pixel_scales=self.source_plane_data_grid.mask.pixel_scales,
origin=self.source_plane_data_grid.mask.origin,
)

return Array2D(
values=mesh_pixels_per_image_pixels,
mask=self.source_plane_data_grid.derive_mask.sub_1,
)
Loading

0 comments on commit 0b79d16

Please sign in to comment.