diff --git a/occamypy/__init__.py b/occamypy/__init__.py index 1b7c6f0..ac33449 100644 --- a/occamypy/__init__.py +++ b/occamypy/__init__.py @@ -1,13 +1,13 @@ from .__version__ import __version__ -from .dask import * -from .numpy import * +from .vector import * from .operator import * +from .numpy import * from .problem import * from .solver import * -from .torch import * +from .dask import * from .utils import * +from .torch import * from .utils import plot -from .vector import * if CUPY_ENABLED: from .cupy import * diff --git a/occamypy/cupy/__init__.py b/occamypy/cupy/__init__.py index 13cbafb..bbad783 100644 --- a/occamypy/cupy/__init__.py +++ b/occamypy/cupy/__init__.py @@ -1,5 +1,5 @@ -from .operator import * from .vector import * +from .operator import * __all__ = [ "VectorCupy", diff --git a/occamypy/cupy/operator/signal.py b/occamypy/cupy/operator/signal.py index d4e7b68..7e49338 100644 --- a/occamypy/cupy/operator/signal.py +++ b/occamypy/cupy/operator/signal.py @@ -1,27 +1,22 @@ from __future__ import division, print_function, absolute_import - -from typing import Union, Tuple - -import cupy as cp import numpy as np +import cupy as cp from cupyx.scipy.ndimage import gaussian_filter - try: from cusignal.convolution import convolve, correlate except ModuleNotFoundError: raise ModuleNotFoundError("cuSIGNAL is not installed. Please install it") -from occamypy.operator.base import Operator, Dstack -from occamypy.vector.base import Vector, superVector -from occamypy.cupy.vector import VectorCupy +from occamypy import Operator, Dstack, Vector, superVector +from occamypy.cupy import VectorCupy class GaussianFilter(Operator): """Gaussian smoothing operator using scipy smoothing""" - - def __init__(self, domain: VectorCupy, sigma: Tuple[float]): + + def __init__(self, model, sigma): """ GaussianFilter (cupy) constructor - + Args: domain: domain vector sigma: standard deviation along the domain directions @@ -29,8 +24,11 @@ def __init__(self, domain: VectorCupy, sigma: Tuple[float]): self.sigma = sigma self.scaling = np.sqrt(np.prod(np.array(self.sigma) / cp.pi)) # in order to have the max amplitude 1 - super(GaussianFilter, self).__init__(domain, domain) - self.name = "GausFilt" + super(GaussianFilter, self).__init__(model, model) + return + + def __str__(self): + return "GausFilt" def forward(self, add, model, data): self.checkDomainRange(model, data) @@ -49,11 +47,11 @@ def adjoint(self, add, model, data): class ConvND(Operator): """ND convolution square operator in the domain space""" - - def __init__(self, domain: VectorCupy, kernel: Union[VectorCupy, cp.ndarray], method: str = 'auto'): + + def __init__(self, domain, kernel, method='auto'): """ ConvND (cupy) constructor - + Args: domain: domain vector kernel: kernel vector @@ -85,7 +83,9 @@ def __init__(self, domain: VectorCupy, kernel: Union[VectorCupy, cp.ndarray], me self.method = method super(ConvND, self).__init__(domain, domain) - self.name = "Convolve" + + def __str__(self): + return " ConvOp " def forward(self, add, model, data): self.checkDomainRange(model, data) @@ -106,7 +106,7 @@ def adjoint(self, add, model, data): return -def Padding(domain: Union[VectorCupy, superVector], pad: Union[Tuple[int], Tuple[Tuple[int]]], mode: str = "constant"): +def Padding(model, pad, mode: str = "constant"): """ Padding operator @@ -119,17 +119,16 @@ def Padding(domain: Union[VectorCupy, superVector], pad: Union[Tuple[int], Tuple pad: number of samples to be added at each end of the dimension, for each dimension mode: padding mode (see https://numpy.org/doc/stable/reference/generated/numpy.pad.html) """ - - if isinstance(domain, VectorCupy): - return _Padding(domain, pad, mode) - elif isinstance(domain, superVector): + if isinstance(model, VectorCupy): + return _Padding(model, pad, mode) + elif isinstance(model, superVector): # TODO add the possibility to have different padding for each sub-vector - return Dstack([_Padding(v, pad, mode) for v in domain.vecs]) + return Dstack([_Padding(v, pad, mode) for v in model.vecs]) else: raise ValueError("ERROR! Provided domain has to be either vector or superVector") -def ZeroPad(domain: VectorCupy, pad: Union[Tuple[int], Tuple[Tuple[int]]]): +def ZeroPad(model, pad): """ Zero-Padding operator @@ -141,8 +140,7 @@ def ZeroPad(domain: VectorCupy, pad: Union[Tuple[int], Tuple[Tuple[int]]]): domain: domain vector pad: number of samples to be added at each end of the dimension, for each dimension """ - - return Padding(domain=domain, pad=pad, mode="constant") + return Padding(model, pad, mode="constant") def _pad_VectorCupy(vec, pad): @@ -159,20 +157,23 @@ def _pad_VectorCupy(vec, pad): class _Padding(Operator): - + def __init__(self, domain, pad, mode: str = "constant"): - - self.dims = domain.shape - pad = [(pad, pad)] * len(self.dims) if pad is cp.isscalar else list(pad) - if (cp.array(pad) < 0).any(): - raise ValueError('Padding must be positive or zero') - self.pad = pad - self.mode = mode - super(_Padding, self).__init__(domain, _pad_VectorCupy(domain, self.pad)) - self.name = "Padding" + + if isinstance(domain, VectorCupy): + self.dims = domain.shape + pad = [(pad, pad)] * len(self.dims) if pad is cp.isscalar else list(pad) + if (cp.array(pad) < 0).any(): + raise ValueError('Padding must be positive or zero') + self.pad = pad + self.mode = mode + super(_Padding, self).__init__(domain, _pad_VectorCupy(domain, self.pad)) + + def __str__(self): + return "Padding " def forward(self, add, model, data): - """Padding the domain""" + """Pad the domain""" self.checkDomainRange(model, data) if add: temp = data.clone() diff --git a/occamypy/cupy/vector.py b/occamypy/cupy/vector.py index b4dfd89..9f0a02a 100644 --- a/occamypy/cupy/vector.py +++ b/occamypy/cupy/vector.py @@ -5,17 +5,16 @@ import numpy as np from GPUtil import getGPUs, getFirstAvailable -from occamypy.vector.base import Vector -from occamypy.numpy.vector import VectorNumpy +from occamypy import Vector, VectorNumpy class VectorCupy(Vector): """Vector class based on cupy.ndarray""" - - def __init__(self, in_content, device: int = None, *args, **kwargs): + + def __init__(self, in_content, device=None, *args, **kwargs): """ VectorCupy constructor - + Args: in_content: numpy.ndarray, cupy.ndarray, tuple or VectorNumpy device: computation device (None for CPU, -1 for least used GPU) @@ -23,6 +22,8 @@ def __init__(self, in_content, device: int = None, *args, **kwargs): **kwargs: dict of arguments for Vector construction """ if isinstance(in_content, cp.ndarray) or isinstance(in_content, np.ndarray): + if cp.isfortran(in_content): + raise TypeError('Input array not a C contiguous array!') self.arr = cp.array(in_content, copy=False) elif isinstance(in_content, tuple): # Tuple size passed to constructor # self.arr = cp.zeros(tuple(reversed(in_content))) @@ -114,16 +115,7 @@ def rand(self, snr=1.): amp_noise = cp.sqrt(3. / snr) * rms # sqrt(3*Power_signal/SNR) self.getNdArray()[:] = amp_noise * (2. * cp.random.random(self.getNdArray().shape) - 1.) return self - - def randn(self, snr=1.): - rms = cp.sqrt(cp.mean(cp.square(self.getNdArray()))) - amp_noise = 1.0 - if rms != 0.: - amp_noise = cp.sqrt(3. / snr) * rms # sqrt(3*Power_signal/SNR) - self.getNdArray()[:] = cp.random.normal(0., 1., self.shape) - self.scale(amp_noise) - return self - + def clone(self): vec_clone = deepcopy(self) # Deep clone of vector # Checking if a vector space was provided @@ -241,6 +233,7 @@ def multiply(self, other): return self def isDifferent(self, other): + # Checking whether the input is a vector or not if not isinstance(other, VectorCupy): raise TypeError("Provided input vector not a %s!" % self.whoami) if not self._check_same_device(other): @@ -260,7 +253,7 @@ def isDifferent(self, other): isDiff = (not cp.equal(self.getNdArray(), other.getNdArray()).all()) return isDiff - def clip(self, low, high): + def clipVector(self, low, high): if not isinstance(low, VectorCupy): raise TypeError("Provided input low vector not a %s!" % self.whoami) if not isinstance(high, VectorCupy): diff --git a/occamypy/dask/__init__.py b/occamypy/dask/__init__.py index 9db7b70..328f602 100644 --- a/occamypy/dask/__init__.py +++ b/occamypy/dask/__init__.py @@ -1,6 +1,6 @@ +from .vector import * from .operator import * from .utils import * -from .vector import * __all__ = [ "DaskClient", diff --git a/occamypy/dask/operator.py b/occamypy/dask/operator.py index 3d7880a..ec3daca 100644 --- a/occamypy/dask/operator.py +++ b/occamypy/dask/operator.py @@ -1,13 +1,11 @@ -from collections.abc import Iterable -from typing import Union, List, Tuple - -import dask.distributed as daskD import numpy as np +import dask.distributed as daskD +from collections.abc import Iterable -from occamypy.operator.base import Operator -from occamypy.vector.base import Vector -from occamypy.dask.utils import DaskClient -from occamypy.dask.vector import DaskVector, scatter_large_data +from occamypy import Vector, Operator +from .vector import DaskVector +from .utils import DaskClient +from .vector import scatter_large_data def call_constructor(constr, args, kwargs=None): @@ -80,7 +78,7 @@ def _check_dask_error(futures): class DaskOperator(Operator): """Class to apply multiple operators in parallel through Dask and DaskVectors""" - def __init__(self, dask_client: DaskClient, op_constructor, op_args: Union[List, Tuple], chunks: Union[List, Tuple], **kwargs): + def __init__(self, dask_client, op_constructor, op_args, chunks, **kwargs): """ DaskOperator constructor @@ -100,6 +98,7 @@ def __init__(self, dask_client: DaskClient, op_constructor, op_args: Union[List, set_aux_name: name of the function to set the auxiliary vector. Useful for VpOperator. spread_op_aux: spreading operator to distribute an auxiliary vector to the set_aux functions """ + # Client to submit tasks if not isinstance(dask_client, DaskClient): raise TypeError("Passed client is not a Dask Client object!") if not isinstance(op_args, list): @@ -219,7 +218,10 @@ def __init__(self, dask_client: DaskClient, op_constructor, op_args: Union[List, if not isinstance(self.SprdAux, DaskSpread): raise TypeError("Provided spread_op_aux not a DaskSpreadOp class!") self.tmp_aux = self.SprdAux.getRange().clone() - self.name = "DaskOp" if isinstance(op_constructor, list) else f"Dask({op_constructor.__name__})" + return + + def __str__(self): + return " DaskOp " def forward(self, add, model, data): if not isinstance(model, DaskVector): @@ -269,7 +271,7 @@ def adjoint(self, add, model, data): def set_background(self, model): """Function to call set_background function of each dask operator""" - if self.set_background_name is None: + if self.set_background_name == None: raise NameError("setbackground_func_name was not defined when constructing the operator!") if self.Sprd: self.Sprd.forward(False, model, self.model_tmp) @@ -284,7 +286,7 @@ def set_background(self, model): def set_aux(self, aux_vec): """Function to call set_nl or set_lin_jac functions of each dask operator""" - if self.set_aux_name is None: + if self.set_aux_name == None: raise NameError("set_aux_name was not defined when constructing the operator!") if self.SprdAux: self.SprdAux.forward(False, aux_vec, self.tmp_aux) @@ -304,14 +306,13 @@ class DaskSpread(Operator): | v1 | | I | fwd: | v2 | = | I | v | v3 | | I | - + adj: | v | = | I | v1 + | I | v2 + | I | v3 """ - - def __init__(self, dask_client: DaskClient, domain: Vector, chunks: Union[List, Tuple]): + def __init__(self, dask_client, domain, chunks): """ DaskSpread constructor - + Args: dask_client: client object to use when submitting tasks (see dask_util module) domain: vector template to be spread/stack (note this is also the domain of the operator) @@ -325,8 +326,11 @@ def __init__(self, dask_client: DaskClient, domain: Vector, chunks: Union[List, self.dask_client = dask_client self.client = self.dask_client.getClient() self.chunks = chunks - super(DaskSpread, self).__init__(domain=domain, range=DaskVector(self.dask_client, vector_template=domain, chunks=chunks)) - self.name = "" + self.setDomainRange(domain, DaskVector(self.dask_client, vector_template=domain, chunks=chunks)) + return + + def __str__(self): + return "DaskSprd" def forward(self, add, model, data): """Distribute local domain through dask""" @@ -340,10 +344,10 @@ def forward(self, add, model, data): # Getting the future to the first vector in the Dask vector modelNd = self.client.submit(getNdfuture, model.vecDask[0], pure=False) else: - # Getting the numpy array to the local domain vector + # Getting the numpy array to the local model vector modelNd = model.getNdArray() - # Spreading domain array to workers + # Spreading model array to workers if len(self.chunks) == self.dask_client.getNworkers(): dataVecList = data.vecDask.copy() for iwrk, wrkId in enumerate(self.dask_client.getWorkerIds()): @@ -376,7 +380,7 @@ def adjoint(self, add, model, data): daskD.wait(self.client.submit(_add_from_NdArray, model, sum_array, pure=False)) else: arrD_list = data.getNdArray() - # Getting the numpy array to the local domain vector + # Getting the numpy array to the local model vector modelNd = model.getNdArray() for arr_i in arrD_list: modelNd[:] += arr_i @@ -386,10 +390,10 @@ def adjoint(self, add, model, data): class DaskCollect(Operator): """Class to Collect/Scatter a Dask vector into/from a local vector""" - def __init__(self, domain: DaskVector, range: Vector): + def __init__(self, domain, range): """ DaskCollect constructor - + Args: domain: dask vector to be collected from remote range: vector class to be locally stored @@ -403,7 +407,6 @@ def __init__(self, domain: DaskVector, range: Vector): if domain.size != range.size: raise ValueError("number of elements in domain and range is not equal!") super(DaskCollect, self).__init__(domain, range) - self.name = "" def forward(self, add, model, data): """Collect dask vector arrays to local one""" diff --git a/occamypy/dask/utils.py b/occamypy/dask/utils.py index 142d884..b9d3067 100644 --- a/occamypy/dask/utils.py +++ b/occamypy/dask/utils.py @@ -18,10 +18,10 @@ def get_tcp_info(filename): """ Obtain the scheduler TCP information - + Args: filename: file to read - + Returns: TCP address """ @@ -33,14 +33,14 @@ def get_tcp_info(filename): return tcp_info -def create_hostnames(machine_names: list, Nworkers: list): +def create_hostnames(machine_names, Nworkers): """ Create hostnames variables (i.e., list of IP addresses) from machine names and number of wokers per machine - + Args: machine_names: list of host names Nworkers: list of client workers - + Returns: list of host IP addresses """ @@ -61,12 +61,12 @@ def create_hostnames(machine_names: list, Nworkers: list): def client_startup(cluster, n_jobs: int, total_workers: int): """ Start a dask client - + Args: cluster: Dask cluster n_jobs: number of jobs to submit to the cluster total_workers: number of total workers in the cluster - + Returns: DaskClient instance, list of workers ID """ @@ -93,7 +93,7 @@ def client_startup(cluster, n_jobs: int, total_workers: int): class DaskClient: """ Dask Client to be used with Dask vectors and operators - + Notes: The Kubernetes pods are created using the Docker image "ettore88/occamypy:devel". To change the image to be use, provide the item image within the kube_params dictionary. @@ -102,34 +102,34 @@ class DaskClient: def __init__(self, **kwargs): """ DaskClient constructor. - + Args: 1) Cluster with shared file system and ssh capability - + hostnames (list) [None]: host names or IP addresses of the machines that the user wants to use in their cluster/client (First hostname will be running the scheduler!) scheduler_file_prefix (str): prefix to used to create dask scheduler-file. logging (bool) [True]: whether to log scheduler and worker stdout to files within dask_logs folder Must be a mounted path on all the machines. Necessary if hostnames are provided [$HOME/scheduler-] - + 2) Local cluster local_params (dict) [None]: Local Cluster options (see help(LocalCluster) for help) n_wrks (int) [1]: number of workers to start - + 3) PBS cluster pbs_params (dict) [None]: PBS Cluster options (see help(PBSCluster) for help) n_jobs (int): number of jobs to be submitted to the cluster n_wrks (int) [1]: number of workers per job - + 4) LSF cluster lfs_params (dict) [None]: LSF Cluster options (see help(LSFCluster) for help) n_jobs (int): number of jobs to be submitted to the cluster n_wrks (int) [1]: number of workers per job - + 5) SLURM cluster slurm_params (dict) [None]: SLURM Cluster options (see help(SLURMCluster) for help) n_jobs (int): number of jobs to be submitted to the cluster n_wrks (int) [1]: number of workers per job - + 6) Kubernetes cluster kube_params (dict): KubeCluster options (see help(KubeCluster) and help(make_pod_spec) for help) [None] @@ -253,19 +253,13 @@ def __init__(self, **kwargs): atexit.register(self.client.shutdown) def getClient(self): - """ - Accessor for obtaining the client object - """ + """Accessor for obtaining the client object""" return self.client def getWorkerIds(self): - """ - Accessor for obtaining the worker IDs - """ + """Accessor for obtaining the worker IDs""" return self.WorkerIds def getNworkers(self): - """ - Accessor for obtaining the number of workers - """ + """Accessor for obtaining the number of workers""" return len(self.getWorkerIds()) diff --git a/occamypy/dask/vector.py b/occamypy/dask/vector.py index 5a195b8..bc43119 100644 --- a/occamypy/dask/vector.py +++ b/occamypy/dask/vector.py @@ -1,17 +1,17 @@ +import numpy as np import os - import dask.distributed as daskD -import numpy as np -from occamypy.numpy.vector import VectorNumpy -from occamypy.utils import sep from occamypy.utils.os import BUF_SIZE -from occamypy.vector.base import Vector -from occamypy.dask.utils import DaskClient +from occamypy.utils import sep +from occamypy import Vector, VectorNumpy +from .utils import DaskClient # Verify if SepVector modules are presents try: import SepVector + + def call_constr_hyper(axes_in): """Function to remotely construct an SepVector using the axis object""" return SepVector.getSepVector(axes=axes_in) @@ -131,6 +131,8 @@ def _call_rand(vecObj): res = vecObj.rand() return res +# todo add _call_randn() + def _call_clone(vecObj): """Function to call clone method""" @@ -236,7 +238,7 @@ def _call_isDifferent(vecObj, vec2): def _call_clipVector(vecObj, low, high): """Function to call multiply method""" - res = vecObj.clip(low, high) + res = vecObj.clipVector(low, high) return res @@ -255,7 +257,7 @@ def checkVector(vec1, vec2): class DaskVector(Vector): - """Definition of a vector object whose computations are performed through a Dask Client""" + """Vector object whose computations are performed through a Dask Client""" def __init__(self, dask_client, **kwargs): """ @@ -367,11 +369,7 @@ def __init__(self, dask_client, **kwargs): daskD.wait(self.vecDask) return - # Class vector operations def getNdArray(self): - """ - Function to return a list of all the arrays of the vector - """ # Retriving arrays by chunks (useful for large arrays) buffer = 27000000 shapes = self.shape @@ -400,20 +398,17 @@ def shape(self): @property def size(self): - """Attribute of total number of elements in the vector""" futures = self.client.map(_call_size, self.vecDask, pure=False) sizes = self.client.gather(futures) return np.sum(sizes) @property def ndim(self): - """Attribute of number of dimensions""" futures = self.client.map(_call_ndim, self.vecDask, pure=False) ndims = self.client.gather(futures) return ndims def norm(self, N=2): - """Function to compute vector N-norm""" norms = self.client.map(_call_norm, self.vecDask, N=N, pure=False) norm = 0.0 for future, result in daskD.as_completed(norms, with_results=True): @@ -421,12 +416,10 @@ def norm(self, N=2): return np.power(norm, 1. / N) def zero(self): - """Function to zero out a vector""" daskD.wait(self.client.map(_call_zero, self.vecDask, pure=False)) return self def max(self): - """Function to obtain maximum value within a vector""" maxs = self.client.map(_call_max, self.vecDask, pure=False) max_val = - np.inf for future, result in daskD.as_completed(maxs, with_results=True): @@ -435,7 +428,6 @@ def max(self): return max_val def min(self): - """Function to obtain minimum value within a vector""" mins = self.client.map(_call_min, self.vecDask, pure=False) min_val = np.inf for future, result in daskD.as_completed(mins, with_results=True): @@ -444,53 +436,38 @@ def min(self): return min_val def set(self, val): - """Function to set all values in the vector""" daskD.wait(self.client.map(_call_set, self.vecDask, val=val, pure=False)) return self def scale(self, sc): - """Function to scale a vector""" daskD.wait(self.client.map(_call_scale, self.vecDask, sc=sc, pure=False)) return self def addbias(self, bias): - """Function to add bias to a vector""" daskD.wait(self.client.map(_call_addbias, self.vecDask, bias=bias, pure=False)) return self def rand(self): - """Function to randomize a vector""" daskD.wait(self.client.map(_call_rand, self.vecDask, pure=False)) return self def clone(self): - """Function to clone (deep copy) a vector from a vector or a Space""" vectors = self.client.map(_call_clone, self.vecDask, pure=False) daskD.wait(vectors) return DaskVector(self.dask_client, dask_vectors=vectors) def cloneSpace(self): - """Function to clone vector space""" vectors = self.client.map(_call_cloneSpace, self.vecDask, pure=False) daskD.wait(vectors) return DaskVector(self.dask_client, dask_vectors=vectors) def checkSame(self, other): - """Function to check to make sure the vectors exist in the same space""" checkVector(self, other) futures = self.client.map(_call_checkSame, self.vecDask, other.vecDask, pure=False) results = self.client.gather(futures) return all(results) def writeVec(self, filename, mode='w', multi_file=False): - """ - Function to write vector to file: - - :param filename : string - Filename to write the vector to - :param mode : string - Writing mode 'w'=overwrite file or 'a'=append to file ['w'] - :param multi_file : boolean - If True multiple files will be written with suffix _chunk1,2,3,...; - otherwise, a single will be written [False] - """ # Check writing mode if not mode in 'wa': raise ValueError("Mode must be appending 'a' or writing 'w' ") @@ -566,56 +543,44 @@ def writeVec(self, filename, mode='w', multi_file=False): return def abs(self): - """Return a vector containing the absolute values""" daskD.wait(self.client.map(_call_abs, self.vecDask, pure=False)) return self def sign(self): - """Return a vector containing the signs""" daskD.wait(self.client.map(_call_sign, self.vecDask, pure=False)) return self def reciprocal(self): - """Return a vector containing the reciprocals of self""" daskD.wait(self.client.map(_call_reciprocal, self.vecDask, pure=False)) return self def conj(self): - """Compute conjugate transpose of the vector""" daskD.wait(self.client.map(_call_conj, self.vecDask, pure=False)) return self def real(self): - """Return the real part of the vector""" daskD.wait(self.client.map(_call_real, self.vecDask, pure=False)) return self def imag(self): - """Return the imaginary part of the vector""" daskD.wait(self.client.map(_call_imag, self.vecDask, pure=False)) return self def pow(self, power): - """Compute element-wise power of the vector""" daskD.wait(self.client.map(_call_pow, self.vecDask, power=power, pure=False)) return self - # Methods combinaning different vectors - def maximum(self, vec2): - """Return a new vector of element-wise maximum of self and vec2""" checkVector(self, vec2) daskD.wait(self.client.map(_call_maximum, self.vecDask, vec2.vecDask, pure=False)) return self def copy(self, other): - """Function to copy vector""" checkVector(self, other) daskD.wait(self.client.map(_call_copy, self.vecDask, other.vecDask, pure=False)) return self def scaleAdd(self, other, sc1=1.0, sc2=1.0): - """Function to scale two vectors and add them to the first one""" checkVector(self, other) sc1 = [sc1] * len(self.vecDask) sc2 = [sc2] * len(self.vecDask) @@ -625,7 +590,6 @@ def scaleAdd(self, other, sc1=1.0, sc2=1.0): return self def dot(self, other): - """Function to compute dot product between two vectors""" checkVector(self, other) dots = self.client.map(_call_dot, self.vecDask, other.vecDask, pure=False) # Adding all the results together @@ -635,22 +599,19 @@ def dot(self, other): return dot def multiply(self, other): - """Function to multiply element-wise two vectors""" checkVector(self, other) futures = self.client.map(_call_multiply, self.vecDask, other.vecDask, pure=False) daskD.wait(futures) return self def isDifferent(self, vec2): - """Function to check if two vectors are identical""" checkVector(self, vec2) futures = self.client.map(_call_isDifferent, self.vecDask, vec2.vecDask, pure=False) results = self.client.gather(futures) return any(results) - def clip(self, low, high): - """Function to bound vector values based on input vectors min and max""" + def clipVector(self, low, high): checkVector(self, low) # Checking low-bound vector checkVector(self, high) # Checking high-bound vector futures = self.client.map(_call_clipVector, self.vecDask, low.vecDask, @@ -659,25 +620,25 @@ def clip(self, low, high): return self -# DASK I/o TO READ LARGE-SCALE VECTORS DIRECTLY WITHIN EACH WORKER -def _get_binaries(**kwargs): +# DASK I/O TO READ LARGE-SCALE VECTORS DIRECTLY WITHIN EACH WORKER +def _get_binaries(filenames, **kwargs): """ Function to obtain associated binary files to each file name - :param filenames: list; List/Array containing file names to read - :return: - binfiles: list; List containing binary files associated to each file - Nbytes: list; List containing the number of bytes within binary files + + Args: + filenames: List/Array containing file names to read + + Returns: + binfiles: list; List containing binary files associated to each file + Nbytes: list; List containing the number of bytes within binary files """ binfiles = list() Nbytes = list() - filenames = kwargs.get("filenames") for filename in filenames: _, ext = os.path.splitext(filename) # Getting file extension if ext == ".H": # SEPlib file binfiles.append(sep.get_binary(filename)) Nbytes.append(os.path.getsize(binfiles[-1])) - elif ext == ".h5": - raise NotImplementedError("ERROR! h5 files not supported yet.") else: raise ValueError("ERROR! Unknown format for file %s" % filename) return binfiles, Nbytes diff --git a/occamypy/numpy/__init__.py b/occamypy/numpy/__init__.py index 4c90487..8783995 100644 --- a/occamypy/numpy/__init__.py +++ b/occamypy/numpy/__init__.py @@ -1,5 +1,5 @@ -from .operator import * from .vector import VectorNumpy +from .operator import * __all__ = [ "VectorNumpy", diff --git a/occamypy/numpy/operator/pylops_interface.py b/occamypy/numpy/operator/pylops_interface.py index 2d34060..c96efab 100644 --- a/occamypy/numpy/operator/pylops_interface.py +++ b/occamypy/numpy/operator/pylops_interface.py @@ -1,12 +1,10 @@ import numpy as np - -from occamypy.numpy.vector import VectorNumpy -from occamypy.operator.base import Operator +from occamypy import Operator try: import pylops -except ModuleNotFoundError: - print("PyLops is not installed. To use this feature please run:\t\npip install pylops") +except ImportError: + raise UserWarning("PyLops is not installed. To use this feature please run: pip install pylops") __all__ = [ "ToPylops", @@ -16,11 +14,11 @@ class FromPylops(Operator): """Cast a pylops.LinearOperator to occamypy.Operator""" - - def __init__(self, domain: VectorNumpy, range: VectorNumpy, op: pylops.LinearOperator): + + def __init__(self, domain, range, op): """ FromPylops constructor - + Args: domain: domain vector range: range vector @@ -37,7 +35,9 @@ def __init__(self, domain: VectorNumpy, range: VectorNumpy, op: pylops.LinearOpe self.op = op super(FromPylops, self).__init__(domain, range) - self.name = self.name.replace("<", "").replace(">", "") + + def __str__(self): + return self.name.replace("<", "").replace(">", "") def forward(self, add, model, data): self.checkDomainRange(model, data) @@ -60,11 +60,11 @@ def adjoint(self, add, model, data): class ToPylops(pylops.LinearOperator): """Cast an numpy-based occamypy.Operator to pylops.LinearOperator""" - + def __init__(self, op: Operator): """ ToPylops constructor - + Args: op: occamypy.Operator """ diff --git a/occamypy/numpy/operator/signal.py b/occamypy/numpy/operator/signal.py index 71b269d..6340ea2 100644 --- a/occamypy/numpy/operator/signal.py +++ b/occamypy/numpy/operator/signal.py @@ -1,21 +1,17 @@ -from typing import Union, Tuple - import numpy as np from scipy.ndimage import gaussian_filter from scipy.signal import convolve, correlate - +from occamypy import superVector, Operator, Dstack from occamypy.numpy.vector import VectorNumpy -from occamypy.operator.base import Operator, Dstack -from occamypy.vector.base import superVector class GaussianFilter(Operator): """Gaussian smoothing operator using scipy smoothing""" - - def __init__(self, model: VectorNumpy, sigma: Tuple[float]): + + def __init__(self, model, sigma): """ GaussianFilter (numpy) constructor - + Args: model: domain vector sigma: standard deviation along the domain directions @@ -23,7 +19,10 @@ def __init__(self, model: VectorNumpy, sigma: Tuple[float]): self.sigma = sigma self.scaling = np.sqrt(np.prod(np.array(self.sigma) / np.pi)) # in order to have the max amplitude 1 super(GaussianFilter, self).__init__(model, model) - self.name = "GausFilt" + return + + def __str__(self): + return "GausFilt" def forward(self, add, model, data): self.checkDomainRange(model, data) @@ -42,11 +41,11 @@ def adjoint(self, add, model, data): class ConvND(Operator): """ND convolution square operator in the domain space""" - - def __init__(self, domain: VectorNumpy, kernel: Union[VectorNumpy, np.ndarray], method: str = 'auto'): + + def __init__(self, model, kernel, method='auto'): """ ConvND (numpy) constructor - + Args: domain: domain vector kernel: kernel vector @@ -70,15 +69,17 @@ def __init__(self, domain: VectorNumpy, kernel: Union[VectorNumpy, np.ndarray], pad_width.append(padding) self.kernel = np.pad(self.kernel, pad_width, mode='constant') - if len(domain.shape) != len(self.kernel.shape): + if len(model.shape) != len(self.kernel.shape): raise ValueError("Domain and kernel number of dimensions mismatch") if method not in ["auto", "direct", "fft"]: raise ValueError("method has to be auto, direct or fft") self.method = method - super(ConvND, self).__init__(domain, domain) - self.name = "Convolve" + super(ConvND, self).__init__(model, model) + + def __str__(self): + return "ConvScipy" def forward(self, add, model, data): self.checkDomainRange(model, data) @@ -99,30 +100,29 @@ def adjoint(self, add, model, data): return -def Padding(domain: Union[VectorNumpy, superVector], pad: Union[Tuple[int], Tuple[Tuple[int]]], mode: str = "constant"): +def Padding(model, pad, mode: str = "constant"): """ Padding operator - + Notes: To pad 2 values to each side of the first dim, and 3 values to each side of the second dim, use: pad=((2,2), (3,3)) - + Args: domain: domain vector pad: number of samples to be added at each end of the dimension, for each dimension mode: padding mode (see https://numpy.org/doc/stable/reference/generated/numpy.pad.html) """ - - if isinstance(domain, VectorNumpy): - return _Padding(domain, pad, mode) - elif isinstance(domain, superVector): + if isinstance(model, VectorNumpy): + return _Padding(model, pad, mode) + elif isinstance(model, superVector): # TODO add the possibility to have different padding for each sub-vector - return Dstack([_Padding(v, pad, mode) for v in domain.vecs]) + return Dstack([_Padding(v, pad, mode) for v in model.vecs]) else: raise ValueError("ERROR! Provided domain has to be either vector or superVector") -def ZeroPad(domain: VectorNumpy, pad: Union[Tuple[int], Tuple[Tuple[int]]]): +def ZeroPad(model, pad): """ Zero-Padding operator @@ -134,8 +134,7 @@ def ZeroPad(domain: VectorNumpy, pad: Union[Tuple[int], Tuple[Tuple[int]]]): domain: domain vector pad: number of samples to be added at each end of the dimension, for each dimension """ - - return Padding(domain=domain, pad=pad, mode="constant") + return Padding(model, pad, mode="constant") def _pad_VectorNumpy(vec, pad): @@ -150,23 +149,35 @@ def _pad_VectorNumpy(vec, pad): class _Padding(Operator): - def __init__(self, domain: VectorNumpy, pad, mode: str = "constant"): - - self.dims = domain.shape + def __init__(self, model: VectorNumpy, pad, mode: str = "constant"): + """ Zero Pad operator. + + To pad 2 values to each side of the first dim, and 3 values to each side of the second dim, use: + pad=((2,2), (3,3)) + :param model: vectorIC class + :param pad: scalar or sequence of scalars + Number of samples to pad in each dimension. + If a single scalar is provided, it is assigned to every dimension. + :param mode: str + Padding mode (see https://numpy.org/doc/stable/reference/generated/numpy.pad.html) + """ + self.dims = model.shape pad = [(pad, pad)] * len(self.dims) if pad is np.isscalar else list(pad) if (np.array(pad) < 0).any(): raise ValueError('Padding must be positive or zero') self.pad = pad self.mode = mode - super(_Padding, self).__init__(domain, _pad_VectorNumpy(domain, self.pad)) - self.name = "Padding" + super(_Padding, self).__init__(model, _pad_VectorNumpy(model, self.pad)) + + def __str__(self): + return "Padding " def forward(self, add, model, data): - """Padding the domain""" + """Pad the domain""" self.checkDomainRange(model, data) if add: temp = data.clone() - y = np.pad(model.getNdArray(), self.pad, mode=self.mode) + y = np.pad(model.arr, self.pad, mode=self.mode) data.arr = y if add: data.scaleAdd(temp, 1., 1.) diff --git a/occamypy/numpy/operator/transform.py b/occamypy/numpy/operator/transform.py index acd57e2..4b46b6c 100644 --- a/occamypy/numpy/operator/transform.py +++ b/occamypy/numpy/operator/transform.py @@ -1,19 +1,16 @@ -from typing import Union, Tuple - import numpy as np -from occamypy.numpy.vector import VectorNumpy -from occamypy.operator.base import Operator +from occamypy import Operator +from ..vector import VectorNumpy class FFT(Operator): """N-dimensional Fast Fourier Transform for complex input""" - - def __init__(self, domain: VectorNumpy, axes: Union[int, Tuple[int]] = None, - nfft: Union[float, Tuple[float]] = None, sampling: Union[float, Tuple[float]] = None): + + def __init__(self, model, axes=None, nfft=None, sampling=None): """ FFT (numpy) constructor - + Args: domain: domain vector axes: index of axes on which the FFT is computed @@ -21,34 +18,36 @@ def __init__(self, domain: VectorNumpy, axes: Union[int, Tuple[int]] = None, sampling: sampling step on each axis """ if axes is None: - axes = tuple(range(domain.ndim)) - elif not isinstance(axes, tuple) and domain.ndim == 1: + axes = tuple(range(model.ndim)) + elif not isinstance(axes, tuple) and model.ndim == 1: axes = (axes,) if nfft is None: - nfft = domain.shape - elif not isinstance(nfft, tuple) and domain.ndim == 1: + nfft = model.shape + elif not isinstance(nfft, tuple) and model.ndim == 1: nfft = (nfft,) if sampling is None: - sampling = tuple([1.] * domain.ndim) - elif not isinstance(sampling, tuple) and domain.ndim == 1: + sampling = tuple([1.] * model.ndim) + elif not isinstance(sampling, tuple) and model.ndim == 1: sampling = (sampling,) if len(axes) != len(nfft) != len(sampling): raise ValueError('axes, nffts, and sampling must have same number of elements') - + self.axes = axes self.nfft = nfft self.sampling = sampling self.fs = [np.fft.fftfreq(n, d=s) for n, s in zip(nfft, sampling)] - dims_fft = np.asarray(domain.shape) + dims_fft = np.asarray(model.shape) for a, n in zip(self.axes, self.nfft): dims_fft[a] = n - super(FFT, self).__init__(domain=VectorNumpy(np.zeros(domain.shape, dtype=complex)), + super(FFT, self).__init__(domain=VectorNumpy(np.zeros(model.shape, dtype=complex)), range=VectorNumpy(np.zeros(shape=dims_fft, dtype=complex))) - self.name = "FFT" + + def __str__(self): + return 'numpyFFT' def forward(self, add, model, data): self.checkDomainRange(model, data) @@ -65,7 +64,7 @@ def adjoint(self, add, model, data): model.zero() modelNd = model.getNdArray() dataNd = data.getNdArray() - # here we need to separate the computation and use np.take for handling nfft > domain.shape + # here we need to separate the computation and use np.take for handling nfft > model.shape temp = np.fft.ifftn(dataNd, s=self.nfft, axes=self.axes, norm='ortho') for a in self.axes: temp = np.take(temp, range(self.domain.shape[a]), axis=a) diff --git a/occamypy/numpy/vector.py b/occamypy/numpy/vector.py index 97c7b4b..43308f4 100644 --- a/occamypy/numpy/vector.py +++ b/occamypy/numpy/vector.py @@ -3,7 +3,7 @@ import numpy as np -from occamypy.vector.base import Vector +from occamypy import Vector class VectorNumpy(Vector): @@ -12,7 +12,7 @@ class VectorNumpy(Vector): def __init__(self, in_content, *args, **kwargs): """ VectorNumpy constructor - + Args: in_content: numpy.ndarray, tuple or path_to_file to load a numpy.ndarray *args: list of arguments for Vector construction @@ -36,7 +36,6 @@ def __init__(self, in_content, *args, **kwargs): self.size = self.arr.size # Total number of elements def getNdArray(self): - """Function to return Ndarray of the vector""" return self.arr def norm(self, N=2): @@ -72,15 +71,6 @@ def rand(self, snr=1.): self.getNdArray()[:] = amp_noise * (2. * np.random.random(self.getNdArray().shape) - 1.) return self - def randn(self, snr=1.): - rms = np.sqrt(np.mean(np.square(self.getNdArray()))) - amp_noise = 1.0 - if rms != 0.: - amp_noise = np.sqrt(3. / snr) * rms # sqrt(3*Power_signal/SNR) - self.getNdArray()[:] = np.random.normal(0., 1., self.shape) - self.scale(amp_noise) - return self - def clone(self): vec_clone = deepcopy(self) # Deep clone of vector # Checking if a vector space was provided @@ -212,10 +202,14 @@ def isDifferent(self, other): isDiff = (not np.array_equal(self.getNdArray(), other.getNdArray())) return isDiff - def clip(self, low, high): + def clipVector(self, low, high): if not isinstance(low, VectorNumpy): raise TypeError("Provided input low vector not a %s!" % self.whoami) if not isinstance(high, VectorNumpy): raise TypeError("Provided input high vector not a %s!" % self.whoami) self.getNdArray()[:] = np.minimum(np.maximum(low.getNdArray(), self.getNdArray()), high.getNdArray()) return self + + def plot(self): + return self.getNdArray() + \ No newline at end of file diff --git a/occamypy/operator/__init__.py b/occamypy/operator/__init__.py index bbbfefe..360bbfa 100644 --- a/occamypy/operator/__init__.py +++ b/occamypy/operator/__init__.py @@ -1,8 +1,8 @@ from .base import * -from .derivative import * from .linear import * -from .matrix import * from .nonlinear import * +from .derivative import * +from .matrix import * __all__ = [ "Operator", diff --git a/occamypy/operator/base.py b/occamypy/operator/base.py index 04e8822..789164d 100644 --- a/occamypy/operator/base.py +++ b/occamypy/operator/base.py @@ -1,27 +1,30 @@ -from copy import deepcopy +from __future__ import division, print_function, absolute_import + from time import time -from typing import Union +from copy import deepcopy import numpy as np import torch -from occamypy.vector.base import Vector, superVector +from occamypy import problem as P +from occamypy import solver as S +from occamypy.vector import Vector, superVector class Operator: """ Abstract python operator class - + Args: domain: domain vector range: range vector - + Attributes: domain: domain vector space range: range vector space name: string that describes the operator H: hermitian operator - + Methods: dot: dot-product with input object getDomain: get domain vector space @@ -33,29 +36,29 @@ class Operator: forward: forward operation adjoint: adjoint (conjugate-tranpose) operation """ - - def __init__(self, domain: Vector, range: Vector): + # Default class methods/functions + def __init__(self, domain, range): """ Operator constructor - + Args: domain: domain vector range: range vector """ self.domain = domain.cloneSpace() self.range = range.cloneSpace() - self.name = "Operator" def __del__(self): """Default destructor""" return def __str__(self): - return self.name + return "Operator" def __repr__(self): return self.__str__() + # unary operators def __add__(self, other): # self + other if isinstance(other, Operator): return _sumOperator(self, other) @@ -73,26 +76,25 @@ def __mul__(self, other): # self * other __rmul__ = __mul__ # other * self - def __truediv__(self, other: Vector, niter: int = 2000): - """x = A / y through CG""" - from occamypy.solver.linear import CG - from occamypy.solver.stopper import BasicStopper - from occamypy.problem.linear import LeastSquares + def __truediv__(self, other, niter=2000): + """x = op / y through CG""" if not self.range.checkSame(other): raise ValueError('Operator range and data domain mismatch') - problem = LeastSquares(model=self.domain.clone().zero(), data=other, op=self) - CGsolver = CG(BasicStopper(niter=niter)) + stopper = S.BasicStopper(niter=niter) + problem = P.LeastSquares(model=self.domain.clone(), data=other, op=self) + CGsolver = S.CG(stopper) CGsolver.run(problem, verbose=False) return problem.model + # main function for all kinds of multiplication def dot(self, other): """Matrix-matrix or matrix-vector or matrix-scalar multiplication.""" - if isinstance(other, Operator): # A * B + if isinstance(other, Operator): # op * B return _prodOperator(self, other) - elif type(other) in [int, float]: # A * c or c * A + elif type(other) in [int, float]: # op * c or c * op return _scaledOperator(self, other) elif isinstance(other, list) and isinstance(self, Vstack): if len(other) != self.n: @@ -102,7 +104,7 @@ def dot(self, other): if len(other) != self.n: raise ValueError("Other lenght and self lenght mismatch") return Hstack([_scaledOperator(self.ops[i], other[i]) for i in range(self.n)]) - elif isinstance(other, Vector) or isinstance(other, superVector): # A * x + elif isinstance(other, Vector) or isinstance(other, superVector): # op * x temp = self.range.clone() self.forward(False, other, temp) return temp @@ -124,7 +126,7 @@ def setDomainRange(self, domain, range): return def checkDomainRange(self, x, y): - """Function to check domain and data vector sizes""" + """Function to check model and data vector sizes""" if not self.domain.checkSame(x): raise ValueError("Provided x vector does not match operator domain") if not self.range.checkSame(y): @@ -132,14 +134,18 @@ def checkDomainRange(self, x, y): def powerMethod(self, verbose=False, tol=1e-8, niter=None, eval_min=False, return_vec=False): """ - Function to estimate maximum eigenvalue of the operator: - - :param return_vec: boolean - Return the estimated eigenvectors [False] - :param niter: int - Maximum number of operator applications [None] - if not provided, the function will continue until the tolerance is reached) - :param eval_min: boolean - Compute the minimum eigenvalue [False] - :param verbose: boolean - Print information to screen as the method is being run [False] - :param tol: float - Tolerance on the change of the estimated eigenvalues [1e-6] + Function to estimate maximum eigenvalue of the operator + + Args: + verbose: verbosity flag + tol: stopping tolerance on the change of the estimated eigenvalues + niter: maximum number of operator applications; + if not provided, the function will continue until the tolerance is reached + eval_min: whether to compute the minimum eigenvalue + return_vec: whether to return the estimated eigenvectors + + Returns: + (eigenvalues, eigenvectors) if return_vec else (eigenvalues) """ # Cloning input and output vectors if verbose: @@ -154,7 +160,7 @@ def powerMethod(self, verbose=False, tol=1e-8, niter=None, eval_min=False, retur pass if not square: if verbose: - print("Note: operator is not square, the eigenvalue is associated to A'A not A!") + print("Note: operator is not square, the eigenvalue is associated to op'op not op!") d_temp = self.range.clone() y = self.domain.clone() # randomize the input vector @@ -170,13 +176,13 @@ def powerMethod(self, verbose=False, tol=1e-8, niter=None, eval_min=False, retur while True: # Applying adjoint if forward not square if square: - self.forward(False, x, y) # y = A x + self.forward(False, x, y) # y = op x else: - self.forward(False, x, d_temp) # d = A x - self.adjoint(False, y, d_temp) # y = A' d = A' A x + self.forward(False, x, d_temp) # d = op x + self.adjoint(False, y, d_temp) # y = op' d = op' op x # Estimating eigenvalue (Rayleigh quotient) - eigen = x.dot(y) # eigen_i = x' A x / (x'x = 1.0) + eigen = x.dot(y) # eigen_i = x' op x / (x'x = 1.0) # x = y x.copy(y) # Normalization of the operator @@ -208,16 +214,16 @@ def powerMethod(self, verbose=False, tol=1e-8, niter=None, eval_min=False, retur eigen = 0.0 # Current estimated eigenvalue eigen_old = 0.0 # Previous estimated eigenvalue # Estimating the minimum eigenvalue - # Shifting all eigenvalues by maximum one (i.e., A_min = A-muI) + # Shifting all eigenvalues by maximum one (i.e., A_min = op-muI) if verbose: print("Starting iterative process for minimum eigenvalue") while True: # Applying adjoint if forward not square if not square: - self.forward(False, x, d_temp) # d = A x - self.adjoint(False, y, d_temp) # y = A' d = A' A x + self.forward(False, x, d_temp) # d = op x + self.adjoint(False, y, d_temp) # y = op' d = op' op x else: - self.forward(False, x, y) # y = A x + self.forward(False, x, y) # y = op x # y = Ax - mu*Ix y.scaleAdd(x, 1.0, -eigen_max) # Estimating eigenvalue (Rayleigh quotient) @@ -251,9 +257,11 @@ def powerMethod(self, verbose=False, tol=1e-8, niter=None, eval_min=False, retur def dotTest(self, verbose=False, tol=1e-4): """ - Function to perform dot-product tests. - :param verbose : boolean; Flag to print information to screen as the method is being run [False] - :param tol : float; The function throws a Warning if the relative error is greater than maxError [1e-4] + Perform the dot-product test + + Args: + verbose: verbosity flag + tol: the function throws a Warning if the relative error is greater than tol """ def _process_complex(x): if isinstance(x, complex): @@ -337,26 +345,12 @@ def _testing(add, dt1, dt2, tol, verbose=False): del d1, d2, r1, r2 return - def forward(self, add: bool, model: Vector, data: Vector): - """ - Forward computation - - Args: - add: whether to add the result to data vector - model: domain vector - data: range vector to store the result - """ + def forward(self, add, model, data): + """Forward operator""" raise NotImplementedError("Forward must be defined") def adjoint(self, add, model, data): - """ - Adjoint computation - - Args: - add: whether to add the result to domain vector - model: domain vector to store the result - data: range vector - """ + """Adjoint operator""" raise NotImplementedError("Adjoint must be defined") def hermitian(self): @@ -364,13 +358,13 @@ def hermitian(self): return _Hermitian(self) H = property(hermitian) - T = H # misleading (H isw the conjugate transpose), probably we can delete it + T = H # misleading (H is the conjugate transpose), probably we can delete it class _Hermitian(Operator): - def __init__(self, op: Operator): - super(_Hermitian, self).__init__(domain=op.range, range=op.domain) + def __init__(self, op): + super(_Hermitian, self).__init__(op.range, op.domain) self.op = op def forward(self, add, model, data): @@ -380,20 +374,20 @@ def adjoint(self, add, model, data): return self.op.forward(add, data, model) -class CustomOperator(Operator): - """Linear operator defined in terms of user-specified operations""" +class _CustomOperator(Operator): + """Linear operator defined in terms of user-specified operations.""" - def __init__(self, domain: Vector, range: Vector, fwd_fn: callable, adj_fn: callable): + def __init__(self, domain, range, fwd_fn, adj_fn): """ CustomOperator constructor - + Args: domain: domain vector range: range vectorr fwd_fn: callable function of the kind f(add, domain, data) adj_fn: callable function of the kind f(add, domain, data) """ - super(CustomOperator, self).__init__(domain=domain, range=range) + super(_CustomOperator, self).__init__(domain, range) self.forward_function = fwd_fn self.adjoint_function = adj_fn @@ -407,7 +401,7 @@ def adjoint(self, add, model, data): class Vstack(Operator): """ Vertical stack of operators - y1 = | A | x + y1 = | op | x y2 | B | """ @@ -437,16 +431,16 @@ def __init__(self, *args): op_range += [self.ops[idx].range] super(Vstack, self).__init__(domain=self.ops[0].domain, range=superVector(op_range)) - self.name = "vstack("+",".join([op.__str__() for op in self.ops]) + ")" + + def __str__(self): + return " VStack " def forward(self, add, model, data): - """Forward operator Cm""" self.checkDomainRange(model, data) for idx in range(self.n): self.ops[idx].forward(add, model, data.vecs[idx]) def adjoint(self, add, model, data): - """Adjoint operator C'r = A'r1 + B'r2""" self.checkDomainRange(model, data) self.ops[0].adjoint(add, model, data.vecs[0]) for idx in range(1, self.n): @@ -456,7 +450,7 @@ def adjoint(self, add, model, data): class Hstack(Operator): """ Horizontal stack of operators - y = [A B] x1 + y = [op B] x1 x2 """ @@ -485,8 +479,10 @@ def __init__(self, *args): raise ValueError('Range incompatibility between Op %d and Op %d' % (idx, idx + 1)) domain += [self.ops[0].domain] super(Hstack, self).__init__(domain=superVector(domain), range=self.ops[0].range) - self.name = "hstack(" + ",".join([op.__str__() for op in self.ops]) + ")" - + + def __str__(self): + return " HStack " + def forward(self, add, model, data): self.checkDomainRange(model, data) self.ops[0].forward(add, model.vecs[0], data) @@ -503,8 +499,7 @@ def adjoint(self, add, model, data): class Dstack(Operator): """ Diagonal stack of operators - - y1 = | A 0 | x1 + y1 = | op 0 | x1 y2 | 0 B | x2 """ @@ -533,16 +528,16 @@ def __init__(self, *args): op_range += [self.ops[idx].range] super(Dstack, self).__init__(domain=superVector(op_domain), range=superVector(op_range)) - self.name = "dstack("+",".join([op.__str__() for op in self.ops]) +")" + + def __str__(self): + return " DStack " def forward(self, add, model, data): - """Forward operator""" self.checkDomainRange(model, data) for idx in range(self.n): self.ops[idx].forward(add, model.vecs[idx], data.vecs[idx]) def adjoint(self, add, model, data): - """Adjoint operator""" self.checkDomainRange(model, data) for idx in range(self.n): self.ops[idx].adjoint(add, model.vecs[idx], data.vecs[idx]) @@ -551,20 +546,21 @@ def adjoint(self, add, model, data): class _sumOperator(Operator): """ Sum of two operators - C = A + B - C.H = A.H + B.H + C = op + B + C.H = op.H + B.H """ - def __init__(self, A: Operator, B: Operator): - """Sum operator constructor""" + def __init__(self, A, B): if not isinstance(A, Operator) or not isinstance(B, Operator): raise TypeError('Both operands have to be a Operator') if not A.range.checkSame(B.range) or not A.domain.checkSame(B.domain): raise ValueError('Cannot add operators: shape mismatch') - super(_sumOperator, self).__init__(domain=A.domain, range=A.range) + super(_sumOperator, self).__init__(A.domain, A.range) self.args = (A, B) - self.name = self.args[0].__str__() + "+" + self.args[1].__str__() + + def __str__(self): + return self.args[0].__str__()[:3] + "+" + self.args[1].__str__()[:4] def forward(self, add, model, data): self.checkDomainRange(model, data) @@ -580,20 +576,22 @@ def adjoint(self, add, model, data): class _prodOperator(Operator): """ Multiplication of two operators - C = A * B - C.H = B.H * A.H + C = op * B + C.H = B.H * op.H """ - def __init__(self, A: Operator, B: Operator): + def __init__(self, A, B): if not isinstance(A, Operator) or not isinstance(B, Operator): raise TypeError('Both operands have to be a Operator') if not A.domain.checkSame(B.range): raise ValueError('Cannot multiply operators: shape mismatch') - super(_prodOperator, self).__init__(domain=B.domain, range=A.range) + super(_prodOperator, self).__init__(B.domain, A.range) self.args = (A, B) self.temp = B.getRange().clone() - self.name = self.args[0].__str__() + "*" + self.args[1].__str__() - + + def __str__(self): + return self.args[0].__str__()[:3] + "*" + self.args[1].__str__()[:4] + def forward(self, add, model, data): self.checkDomainRange(model, data) self.args[1].forward(False, model, self.temp) @@ -606,34 +604,31 @@ def adjoint(self, add, model, data): class _scaledOperator(Operator): - """Scaled operator B = c A""" + """Scaled operator B = c op""" - def __init__(self, op: Operator, const: Union[float, int]): + def __init__(self, op, const): """ ScaledOperator constructor. - + Args: op: operator const: scaling factor """ if not isinstance(op, Operator): - raise TypeError('Operator expected as A') + raise TypeError('Operator expected as op') if not type(const) in [int, float]: raise ValueError('scalar expected as const') - super(_scaledOperator, self).__init__(domain=op.domain, range=op.range) + super(_scaledOperator, self).__init__(op.domain, op.range) self.const = const self.op = op - - self.name = self._build_name() - - def _build_name(self): + + def __str__(self): op_name = self.op.__str__().replace(" ", "") op_name_len = len(op_name) - # if op_name_len <= 6: - # name = "sc" + op_name + "" * (6 - op_name_len) - # else: - # name = "sc" + op_name[:6] - name = "sc_" + op_name + if op_name_len <= 6: + name = "sc" + op_name + "" * (6 - op_name_len) + else: + name = "sc" + op_name[:6] return name def forward(self, add, model, data): @@ -643,18 +638,18 @@ def adjoint(self, add, model, data): self.op.adjoint(add, model, data.clone().scale(np.conj(self.const))) -# for backward compatibility def Chain(A, B): """ Chain of two linear operators: - d = B A m - + d = B op m + Notes: - this function is deprecated, as you can simply write (B * A). Watch out for the order! + this function is deprecated, as you can simply write (B * op). Watch out for the order! """ return _prodOperator(B, A) +# for backward compatibility Transpose = Operator.H sumOperator = _sumOperator stackOperator = Vstack diff --git a/occamypy/operator/derivative.py b/occamypy/operator/derivative.py index 0437ad4..aed8894 100644 --- a/occamypy/operator/derivative.py +++ b/occamypy/operator/derivative.py @@ -1,14 +1,11 @@ -from typing import Union, Tuple - -from occamypy.operator.base import Operator, Vstack from occamypy.utils import get_backend -from occamypy.vector.base import Vector +from .base import Operator, Vstack class FirstDerivative(Operator): r""" First Derivative with a stencil - + 1) 2nd order centered: .. math:: @@ -24,8 +21,7 @@ class FirstDerivative(Operator): .. math:: y[i] = 0.5 (x[i] - x[i-1]) / dx """ - - def __init__(self, domain: Vector, sampling: float = 1., axis: int = 0, stencil: str = 'centered'): + def __init__(self, model, sampling=1., axis=0, stencil='centered'): """ FirstDerivative costructor @@ -36,7 +32,7 @@ def __init__(self, domain: Vector, sampling: float = 1., axis: int = 0, stencil: stencil: derivative kind (centered, forward, backward) """ self.sampling = sampling - self.dims = domain.getNdArray().shape + self.dims = model.getNdArray().shape self.axis = axis if axis >= 0 else len(self.dims) + axis self.stencil = stencil @@ -52,10 +48,12 @@ def __init__(self, domain: Vector, sampling: float = 1., axis: int = 0, stencil: else: raise ValueError("Derivative stencil must be centered, forward or backward") - self.backend = get_backend(domain) + self.backend = get_backend(model) - super(FirstDerivative, self).__init__(domain, domain) - self.name = "1stDer_%d" % self.axis + super(FirstDerivative, self).__init__(model, model) + + def __str__(self): + return "1stDer_%d" % self.axis def _forwardF(self, add, model, data): self.checkDomainRange(model, data) @@ -167,25 +165,26 @@ class SecondDerivative(Operator): .. math:: y[i] = (x[i+1] - 2x[i] + x[i-1]) / dx^2 """ - - def __init__(self, domain: Vector, sampling: float = 1., axis: int = 0): + def __init__(self, model, sampling=1., axis=0): """ SecondDerivative constructor - + Args: domain: domain vector sampling: sampling step along the differentiation axis axis: axis along which to compute the derivative """ self.sampling = sampling - self.data_tmp = domain.clone().zero() - self.dims = domain.getNdArray().shape + self.data_tmp = model.clone().zero() + self.dims = model.getNdArray().shape self.axis = axis if axis >= 0 else len(self.dims) + axis - self.backend = get_backend(domain) + self.backend = get_backend(model) - super(SecondDerivative, self).__init__(domain=domain, range=domain) - self.name = "2ndDer_%d" % self.axis + super(SecondDerivative, self).__init__(model, model) + + def __str__(self): + return "2ndDer_%d" % self.axis def forward(self, add, model, data): self.checkDomainRange(model, data) @@ -228,17 +227,17 @@ def adjoint(self, add, model, data): class Gradient(Operator): """N-Dimensional Gradient operator""" - - def __init__(self, domain: Vector, sampling: Union[Tuple[float], float] = None, stencil: Union[Tuple[str], str] = None): + + def __init__(self, model, sampling=None, stencil=None): """ Gradient constructor - + Args: domain: domain vector sampling: sampling steps stencil: stencil kind for each direction """ - self.dims = domain.getNdArray().shape + self.dims = model.getNdArray().shape self.sampling = sampling if sampling is not None else tuple([1] * len(self.dims)) if stencil is None: @@ -254,10 +253,12 @@ def __init__(self, domain: Vector, sampling: Union[Tuple[float], float] = None, if len(self.sampling) != len(self.stencil): raise ValueError("There is something wrong with the dimensions") - self.op = Vstack([FirstDerivative(domain, sampling=self.sampling[d], axis=d) + self.op = Vstack([FirstDerivative(model, sampling=self.sampling[d], axis=d) for d in range(len(self.dims))]) super(Gradient, self).__init__(domain=self.op.domain, range=self.op.range) - self.name = "Gradient" + + def __str__(self): + return "Gradient" def forward(self, add, model, data): return self.op.forward(add, model, data) @@ -285,24 +286,24 @@ def merge_directions(self, grad_vector, iso=True): class Laplacian(Operator): - r""" + """ Laplacian operator. - + Notes: The input parameters are tailored for >2D, but it works also for 1D. """ - def __init__(self, domain: Vector, axis: Tuple[int] = None, weights: Tuple[float] = None, sampling: Tuple[float] = None): + def __init__(self, model, axis=None, weights=None, sampling=None): """ Laplacian constructor - + Args: domain: domain vector axis: axes along which to compute the derivative weights: scalar weights for each axis sampling: sampling steps for each axis """ - self.dims = domain.shape + self.dims = model.getNdArray().shape self.axis = axis if axis is not None else tuple(range(len(self.dims))) self.sampling = sampling if sampling is not None else tuple([1] * len(self.dims)) self.weights = weights if weights is not None else tuple([1] * len(self.dims)) @@ -310,13 +311,15 @@ def __init__(self, domain: Vector, axis: Tuple[int] = None, weights: Tuple[float if not (len(self.axis) == len(self.weights) == len(self.sampling)): raise ValueError("There is something wrong with the dimensions") - self.data_tmp = domain.clone().zero() + self.data_tmp = model.clone().zero() - self.op = self.weights[0] * SecondDerivative(domain, sampling=self.sampling[0], axis=self.axis[0]) + self.op = self.weights[0] * SecondDerivative(model, sampling=self.sampling[0], axis=self.axis[0]) for d in range(1, len(self.axis)): - self.op += self.weights[d] * SecondDerivative(domain, sampling=self.sampling[d], axis=self.axis[d]) - super(Laplacian, self).__init__(domain, domain) - self.name = "Laplacian" + self.op += self.weights[d] * SecondDerivative(model, sampling=self.sampling[d], axis=self.axis[d]) + super(Laplacian, self).__init__(model, model) + + def __str__(self): + return "Laplace " def forward(self, add, model, data): return self.op.forward(add, model, data) diff --git a/occamypy/operator/linear.py b/occamypy/operator/linear.py index ad3aafa..246ac06 100644 --- a/occamypy/operator/linear.py +++ b/occamypy/operator/linear.py @@ -1,24 +1,22 @@ -from typing import Union - import numpy as np - -from occamypy.operator.base import Operator -from occamypy.vector.base import Vector +from .base import Operator class Zero(Operator): """Zero matrix operator; useful for Jacobian matrices that are zeros""" - def __init__(self, domain: Vector, range: Vector): + def __init__(self, domain, range): """ Zero constructor - + Args: domain: domain vector range: range vector """ - super(Zero, self).__init__(domain=domain, range=range) - self.name = "Zero" + super(Zero, self).__init__(domain, range) + + def __str__(self): + return " Zero " def forward(self, add, model, data): self.checkDomainRange(model, data) @@ -34,15 +32,17 @@ def adjoint(self, add, model, data): class Identity(Operator): """Identity operator""" - def __init__(self, domain: Vector): + def __init__(self, domain): """ Identity constructor - + Args: domain: domain vector """ - super(Identity, self).__init__(domain=domain, range=domain) - self.name = "Identity" + super(Identity, self).__init__(domain, domain) + + def __str__(self): + return "Identity" def forward(self, add, model, data): self.checkDomainRange(model, data) @@ -62,19 +62,21 @@ def adjoint(self, add, model, data): class Scaling(Operator): """scalar multiplication operator""" - def __init__(self, domain: Vector, scalar: Union[float, int]): + def __init__(self, domain, scalar): """ Scaling constructor - + Args: domain: domain vector scalar: scaling coefficient """ - super(Scaling, self).__init__(domain=domain, range=domain) + super(Scaling, self).__init__(domain, domain) if not np.isscalar(scalar): raise ValueError('scalar has to be (indeed) a scalar variable') self.scalar = scalar - self.name = "Scaling" + + def __str__(self): + return "Scaling " def forward(self, add, model, data): self.checkDomainRange(model, data) @@ -88,18 +90,18 @@ def adjoint(self, add, model, data): class Diagonal(Operator): """Diagonal operator for performing element-wise multiplication""" - def __init__(self, diag: Vector): + def __init__(self, diag): """ Diagonal constructor - + Args: diag: vector to be stored on the diagonal """ - # if not isinstance(diag, vector): - # raise TypeError('diag has to be a vector') - super(Diagonal, self).__init__(domain=diag, range=diag) + super(Diagonal, self).__init__(diag, diag) self.diag = diag - self.name = "Diagonal" + + def __str__(self): + return "Diagonal" def forward(self, add, model, data): self.checkDomainRange(model, data) diff --git a/occamypy/operator/matrix.py b/occamypy/operator/matrix.py index 1de97c5..872ad6a 100644 --- a/occamypy/operator/matrix.py +++ b/occamypy/operator/matrix.py @@ -1,20 +1,19 @@ -from occamypy.operator.base import Operator +from occamypy import Vector +from .base import Operator from occamypy.utils import get_backend, get_vector_type -from occamypy.vector.base import Vector class Matrix(Operator): """ Linear Operator build upon an explicit matrix - + Attributes: matrix: Vector array that contains the matrix """ - - def __init__(self, matrix: Vector, domain: Vector, range: Vector, outcore: bool = False): + def __init__(self, matrix: Vector, domain: Vector, range: Vector, outcore=False): """ Matrix constructor - + Args: matrix: vector that contains the matrix domain: domain vector @@ -35,11 +34,11 @@ def __init__(self, matrix: Vector, domain: Vector, range: Vector, outcore: bool self.matrix = matrix self.outcore = outcore - - self.name = "Matrix" + + def __str__(self): + return "MatrixOp" def forward(self, add, model, data): - """Forward multiplication: d = A * m""" self.checkDomainRange(model, data) if not add: data.zero() @@ -47,7 +46,6 @@ def forward(self, add, model, data): return def adjoint(self, add, model, data): - """Adjoint multiplication: m = A' * d""" self.checkDomainRange(model, data) if not add: model.zero() diff --git a/occamypy/operator/nonlinear.py b/occamypy/operator/nonlinear.py index d619bf3..2d7e74e 100644 --- a/occamypy/operator/nonlinear.py +++ b/occamypy/operator/nonlinear.py @@ -1,6 +1,6 @@ import numpy as np -from occamypy.operator.base import Operator, Vstack, _sumOperator, _prodOperator +from .base import Operator, Vstack, _sumOperator, _prodOperator def dummy_set_background(dummy_arg): @@ -13,7 +13,7 @@ def dummy_set_background(dummy_arg): class NonlinearOperator(Operator): """ Non-linear operator - + Methods: linTest: perform linearization test """ @@ -21,7 +21,7 @@ class NonlinearOperator(Operator): def __init__(self, nl_op, lin_op=None, set_background_func=dummy_set_background): """ NonlinearOperator - + Args: nl_op: Operator where only the forward is defined lin_op: Jacobian operator (if not necessary, use occamypy.Zero) @@ -37,12 +37,11 @@ def __init__(self, nl_op, lin_op=None, set_background_func=dummy_set_background) if not self.nl_op.range.checkSame(self.lin_op.range): raise ValueError("ERROR! The two provided operators have different ranges") super(NonlinearOperator, self).__init__(self.nl_op.domain, self.nl_op.range) - self.name = "NLOperator" - + def dotTest(self, **kwargs): raise NotImplementedError("Perform dot-product tests directly on the linear operator.") - def linTest(self, background, pert=None, alpha=np.logspace(-6, 0, 100), plot: bool = False): + def linTest(self, background, pert=None, alpha=np.logspace(-6, 0, 100), plot=False): r""" Compute the linearization error of the operator as @@ -58,7 +57,7 @@ def linTest(self, background, pert=None, alpha=np.logspace(-6, 0, 100), plot: bo Returns: alpha array, error array """ - # Creating domain perturbation if not provided + # Creating model perturbation if not provided if pert is None: pert = self.getDomain().clone().rand() # List containing linearization error and perturbation scale/norm @@ -89,17 +88,17 @@ def linTest(self, background, pert=None, alpha=np.logspace(-6, 0, 100), plot: bo lin_err = np.array(lin_err) if plot: import matplotlib.pyplot as plt - fig, ax = plt.subplots(figsize=(6, 4)) - ax.plot(alpha, lin_err, 'r') - ax.set_xlim(0,1), ax.set_ylim(0) - ax.set_xlabel(r"$\alpha$") - ax.set_ylabel(r"$|f(m_0+\alpha dm) - f(m_0) - \alpha F(m_0)dm|_2$") - ax.grid(True) - plt.suptitle('Linearization error') - plt.tight_layout() + fig, ax = plt.subplots(figsize=(6, 3)) + plt.plot(alpha, lin_err, 'r') + ax.autoscale(enable=True, axis='y', tight=True) + ax.autoscale(enable=True, axis='x', tight=True) + plt.xlabel(r"$\alpha$") + plt.ylabel(r"$|f(m_0+\alpha dm) - f(m_0) - \alpha F(m_0)dm|_2$") + plt.title('Linearization error') plt.show() return alpha, lin_err + # unary operators def __add__(self, other): # self + other if isinstance(other, NonlinearOperator): return _sumNlOperator(self, other) @@ -114,11 +113,10 @@ class NonlinearComb(NonlinearOperator): .. math:: f(g(\mathbf{m})) """ - def __init__(self, f, g): """ NonlinearComb constructor - + Args: f: last operator to be applied g: first operator to be applied @@ -137,12 +135,9 @@ def __init__(self, f, g): self.g_nl_op = g.nl_op self.g_range_tmp = g.nl_op.range.clone() super(NonlinearComb, self).__init__(self.nl_op, self.lin_op, self.set_background) - self.name = f.__str__()+"("+g.__str__()+")" - + def set_background(self, model): - """ - Set background function for the chain of Jacobian matrices - """ + """ Set background function for the chain of Jacobian matrices""" # Setting G(m0) self.set_background_g(model) # Setting F(g(m0)) @@ -150,15 +145,15 @@ def set_background(self, model): self.set_background_f(self.g_range_tmp) -# Necessary for backward compatibility def CombNonlinearOp(g, f): + """CombNonlinearOp is deprecated! Please use NonlinearComb instead""" return NonlinearComb(f, g) class _sumNlOperator(NonlinearOperator): - """ + r""" Sum of two non-linear operators - + .. math:: h = g + f """ @@ -166,7 +161,7 @@ class _sumNlOperator(NonlinearOperator): def __init__(self, g, f): """ sumNlOperator constructor - + Args: g: first operator f: second operator @@ -188,12 +183,11 @@ def __init__(self, g, f): self.g_nl_op = g.nl_op self.g_range_tmp = g.nl_op.range.clone() super(_sumNlOperator, self).__init__(self.nl_op, self.lin_op, self.set_background) - self.name = self.args[0].__str__() + "+" + self.args[1].__str__() + + def __str__(self): + return self.args[0].__str__()[:3] + "+" + self.args[1].__str__()[:4] def set_background(self, model): - """ - Set background function for the sum of Jacobian matrices - """ # Setting G(m0) self.set_background_g(model) # Setting F(m0) @@ -207,16 +201,15 @@ def NonlinearSum(f, g): class NonlinearVstack(NonlinearOperator): """ Stack of operators class - - | d1 | | f(m) | - h(m) = | | = | | - | d2 | | g(m) | + + | d1 | | f(m) | + h(m) = | | = | | + | d2 | | g(m) | """ - def __init__(self, nl_op1, nl_op2): """ NonlinearVstack constructor - + Args: nl_op1: first operator nl_op2: second operator @@ -234,7 +227,9 @@ def __init__(self, nl_op1, nl_op2): self.set_background1 = nl_op1.set_background self.set_background2 = nl_op2.set_background super(NonlinearVstack, self).__init__(self.nl_op, self.lin_op, self.set_background) - self.name = "vstack("+",".join([nl_op1.__str__(), nl_op2.__str__()]) + ")" + + def __str__(self): + return "NLVstack" def set_background(self, model): # Setting F(m0) @@ -246,12 +241,12 @@ def set_background(self, model): class VarProOperator(Operator): r""" Variable-projection Operator of the form: - + .. math:: h(\mathbf{m}_nl) \mathbf{m}_lin """ - def __init__(self, h_nl: NonlinearOperator, h_lin: Operator, set_nl, set_lin_jac, set_lin=None): + def __init__(self, h_nl, h_lin, set_nl, set_lin_jac, set_lin=None): """ VarProOperator constructor @@ -272,7 +267,6 @@ def __init__(self, h_nl: NonlinearOperator, h_lin: Operator, set_nl, set_lin_jac self.set_nl = set_nl # Function to set the non-linear component of the h(m_nl) self.set_lin_jac = set_lin_jac # Function to set the non-linear component of the Jacobian H(m_nl;m_lin) self.set_lin = set_lin # Function to set the non-linear component h(m_nl)m_lin - self.name = "VarProOp" def dotTest(self, verb=False, maxError=.0001): raise NotImplementedError("ERROR! Perform dot-product tests directly onto linear operator and Jacobian of h(m_nl).") @@ -284,10 +278,8 @@ class cosOperator(Operator): def __init__(self, domain): super(cosOperator, self).__init__(domain, domain) - self.name = "Cosine" - + def forward(self, add, model, data): - """Forward operator cos(x)""" self.checkDomainRange(model, data) if not add: data.zero() @@ -302,10 +294,8 @@ def __init__(self, domain): super(cosJacobian, self).__init__(domain, domain) self.background = domain.clone() self.backgroundNd = self.background.getNdArray() - self.name = "J(cosine)" - + def forward(self, add, model, data): - """Forward operator""" self.checkDomainRange(model, data) if not add: data.zero() @@ -313,7 +303,6 @@ def forward(self, add, model, data): return def adjoint(self, add, model, data): - """Adjoint operator""" self.checkDomainRange(model, data) if not add: model.zero() @@ -321,6 +310,5 @@ def adjoint(self, add, model, data): return def set_background(self, background): - """ Setting -sin(x0)""" self.background.copy(background) return diff --git a/occamypy/problem/base.py b/occamypy/problem/base.py index e93fc04..8334dfe 100644 --- a/occamypy/problem/base.py +++ b/occamypy/problem/base.py @@ -9,7 +9,7 @@ class Bounds: def __init__(self, minBound=None, maxBound=None): """ Bounds constructor - + Args: minBound: vector containing minimum values of the domain vector maxBound: vector containing maximum values of the domain vector @@ -25,28 +25,28 @@ def __init__(self, minBound=None, maxBound=None): self.minBound.scale(-1.0) return - def apply(self, in_content): + def apply(self, input_vec): """ Apply bounds to the input vector - + Args: in_content: vector to be processed """ if self.minBound is not None and self.maxBound is None: - if not in_content.checkSame(self.minBound): + if not input_vec.checkSame(self.minBound): raise ValueError("Input vector not consistent with bound space") - in_content.scale(-1.0) - in_content.clip(in_content, self.minBound) - in_content.scale(-1.0) + input_vec.scale(-1.0) + input_vec.clipVector(input_vec, self.minBound) + input_vec.scale(-1.0) elif self.minBound is None and self.maxBound is not None: - if not in_content.checkSame(self.maxBound): + if not input_vec.checkSame(self.maxBound): raise ValueError("Input vector not consistent with bound space") - in_content.clip(in_content, self.maxBound) + input_vec.clipVector(input_vec, self.maxBound) elif self.minBound is not None and self.maxBound is not None: - if (not (in_content.checkSame(self.minBound) and in_content.checkSame( + if (not (input_vec.checkSame(self.minBound) and input_vec.checkSame( self.maxBound))): raise ValueError("Input vector not consistent with bound space") - in_content.clip(self.minBound, self.maxBound) + input_vec.clipVector(self.minBound, self.maxBound) return @@ -56,7 +56,7 @@ class Problem: def __init__(self, minBound=None, maxBound=None, boundProj=None): """ Problem constructor - + Args: minBound: vector containing minimum values of the domain vector maxBound: vector containing maximum values of the domain vector @@ -79,6 +79,7 @@ def __init__(self, minBound=None, maxBound=None, boundProj=None): self.linear = False # By default all problem are non-linear def __del__(self): + """Default destructor""" return def setDefaults(self): @@ -92,28 +93,30 @@ def setDefaults(self): self.counter = 0 return - def set_model(self, in_content): - """Setting internal domain vector - + def set_model(self, model): + """ + Setting internal domain vector + Args: in_content: domain vector to be copied """ - if in_content.isDifferent(self.model): - self.model.copy(in_content) + if model.isDifferent(self.model): + self.model.copy(model) self.obj_updated = False self.res_updated = False self.grad_updated = False self.dres_updated = False - def set_residual(self, in_content): - """Setting internal residual vector - + def set_residual(self, residual): + """ + Setting internal residual vector + Args: in_content: residual vector to be copied """ # Useful for linear inversion (to avoid residual computation) - if self.res.isDifferent(in_content): - self.res.copy(in_content) + if self.res.isDifferent(residual): + self.res.copy(residual) # If residuals have changed, recompute gradient and objective function value self.grad_updated = False self.obj_updated = False @@ -121,30 +124,33 @@ def set_residual(self, in_content): return def get_model(self): - """Get the domain vector""" + """Geet the domain vector""" return self.model def get_dmodel(self): - """Get the domain vector""" + """Get the model perturbation vector""" return self.dmodel - def get_rnorm(self, model) -> float: - """Compute the residual vector norm + def get_rnorm(self, model): + """ + Compute the residual vector norm + Args: model: domain vector """ self.get_res(model) return self.get_res(model).norm() - def get_gnorm(self, model) -> float: - """Compute the gradient vector norm - + def get_gnorm(self, model): + """ + Compute the gradient vector norm + Args: model: domain vector """ return self.get_grad(model).norm() - def get_obj(self, model) -> float: + def get_obj(self, model): """Compute the objective function Args: @@ -158,8 +164,9 @@ def get_obj(self, model) -> float: return self.obj def get_res(self, model): - """Compute the residual vector - + """ + Compute the residual vector + Args: model: domain vector """ @@ -171,8 +178,9 @@ def get_res(self, model): return self.res def get_grad(self, model): - """Compute the gradient vector - + """ + Compute the gradient vector + Args: model: domain vector """ @@ -187,11 +195,11 @@ def get_grad(self, model): return self.grad def get_dres(self, model, dmodel): - """Compute the dresidual vector (i.e., application of the Jacobian to Dmodel vector) - + """Compute the perturbation residual vector (i.e., application of the Jacobian to model perturbation vector) + Args: model: domain vector - dmodel: dmodel vector + dmodel: domain perturbation vector """ self.set_model(model) if not self.dres_updated or dmodel.isDifferent(self.dmodel): @@ -210,9 +218,10 @@ def get_gevals(self): """Get the number of gradient evalutions""" return self.gevals - def objf(self, residual) -> float: - """Compute the objective function - + def objf(self, residual): + """ + Compute the objective function + Args: residual: residual vector Returns: @@ -236,7 +245,7 @@ def dresf(self, model, dmodel): Compute the residual vector Args: model: domain vector - dmodel: dmodel vector + dmodel: domain perturbation vector Returns: residual vector """ diff --git a/occamypy/problem/linear.py b/occamypy/problem/linear.py index 81332d1..320d3b7 100644 --- a/occamypy/problem/linear.py +++ b/occamypy/problem/linear.py @@ -1,8 +1,8 @@ from math import isnan -from occamypy.operator import Identity, Vstack -from occamypy.problem.base import Problem -from occamypy.vector.base import superVector, Vector +from .base import Problem +from occamypy import operator as O +from occamypy.vector import superVector class LeastSquares(Problem): @@ -13,8 +13,8 @@ class LeastSquares(Problem): \frac{1}{2} \Vert \mathbf{A}\mathbf{m} - \mathbf{d}\Vert_2^2 """ - def __init__(self, model: Vector, data: Vector, op, grad_mask: Vector = None, prec=None, - minBound: Vector = None, maxBound: Vector = None, boundProj=None): + def __init__(self, model, data, op, grad_mask=None, prec=None, + minBound=None, maxBound=None, boundProj=None): """ LeastSquares constructor @@ -49,7 +49,7 @@ def __init__(self, model: Vector, data: Vector, op, grad_mask: Vector = None, pr self.grad_mask = grad_mask if self.grad_mask is not None: if not grad_mask.checkSame(model): - raise ValueError("Mask size not consistent with domain vector!") + raise ValueError("Mask size not consistent with model vector!") self.grad_mask = grad_mask.clone() # Preconditioning matrix self.prec = prec @@ -57,14 +57,15 @@ def __init__(self, model: Vector, data: Vector, op, grad_mask: Vector = None, pr self.setDefaults() self.linear = True return - + def __del__(self): + """Default destructor""" return - + def resf(self, model): r""" Method to return residual vector - + .. math:: \mathbf{r} = \mathbf{A} \mathbf{m} - \mathbf{d} """ @@ -76,11 +77,11 @@ def resf(self, model): # Computing Am - d self.res.scaleAdd(self.data, 1., -1.) return self.res - + def gradf(self, model, res): r""" Method to return gradient vector - + .. math:: \mathbf{g} = \mathbf{A}'\mathbf{r} = \mathbf{A}'(\mathbf{A} \mathbf{m} - \mathbf{d}) """ @@ -90,23 +91,24 @@ def gradf(self, model, res): if self.grad_mask is not None: self.grad.multiply(self.grad_mask) return self.grad - + def dresf(self, model, dmodel): r""" Method to return residual vector - + .. math:: \mathbf{r}_d = \mathbf{A} \mathbf{r}_m """ + # Computing A dm = dres self.op.forward(False, dmodel, self.dres) return self.dres - + def objf(self, residual): r""" Method to return objective function value - + .. math:: - \frac{1}{2} \Vert \mathbf{A}\mathbf{m} - \mathbf{d}\Vert_2^2 + \frac{1}{2} \Vert \mathbf{r} \Vert_2^2 """ val = residual.norm() obj = 0.5 * val * val @@ -116,15 +118,15 @@ def objf(self, residual): class LeastSquaresSymmetric(Problem): r""" Linear inverse problem of the form - + .. math:: \frac{1}{2} \Vert \mathbf{A}\mathbf{m} - \mathbf{d}\Vert_2^2 - + where A is a symmetric operator (i.e., A' = A) """ - def __init__(self, model: Vector, data: Vector, op, prec=None, - minBound: Vector = None, maxBound: Vector = None, boundProj=None): + def __init__(self, model, data, op, prec=None, + minBound=None, maxBound=None, boundProj=None): """ LeastSquaresSymmetric constructor @@ -141,7 +143,7 @@ def __init__(self, model: Vector, data: Vector, op, prec=None, super(LeastSquaresSymmetric, self).__init__(minBound, maxBound, boundProj) # Checking range and domain are the same if not model.checkSame(data) and not op.domain.checkSame(op.range): - raise ValueError("Data and domain vector live in different spaces!") + raise ValueError("Data and model vector live in different spaces!") # Setting internal vector self.model = model self.dmodel = model.clone() @@ -162,7 +164,11 @@ def __init__(self, model: Vector, data: Vector, op, prec=None, # Setting default variables self.setDefaults() self.linear = True - + + def __del__(self): + """Default destructor""" + return + def resf(self, model): r""" Method to return residual vector @@ -170,38 +176,40 @@ def resf(self, model): .. math:: \mathbf{r} = \mathbf{A} \mathbf{m} - \mathbf{d} """ + # Computing Am if model.norm() != 0.: self.op.forward(False, model, self.res) else: self.res.zero() - + # Computing Am - d self.res.scaleAdd(self.data, 1., -1.) return self.res - + def gradf(self, model, res): r"""Method to return gradient vector - + .. math:: \mathbf{g} = \mathbf{r} """ + # Assigning g = r self.grad = self.res return self.grad - + def dresf(self, model, dmodel): r""" Method to return residual vector - + .. math:: \mathbf{r}_d = \mathbf{A} \mathbf{r}_m """ # Computing Ldm = dres self.op.forward(False, dmodel, self.dres) return self.dres - + def objf(self, residual): r""" Method to return objective function value - + .. math:: \frac{1}{2} [ \mathbf{m}'\mathbf{A}\mathbf{m} - \mathbf{m}'\mathbf{d}] """ @@ -212,14 +220,13 @@ def objf(self, residual): class LeastSquaresRegularized(Problem): r""" Linear regularized inverse problem of the form - + .. math:: \frac{1}{2} \Vert \mathbf{A}\mathbf{m} - \mathbf{d}\Vert_2^2 + \frac{\varepsilon^2}{2} \Vert \mathbf{R m} - \mathbf{m}_p \Vert_2^2 """ - def __init__(self, model: Vector, data: Vector, op, epsilon: float, grad_mask: Vector = None, reg_op=None, - prior_model: Vector = None, prec=None, - minBound: Vector = None, maxBound: Vector = None, boundProj=None): + def __init__(self, model, data, op, epsilon, grad_mask=None, reg_op=None, prior_model=None, prec=None, + minBound=None, maxBound=None, boundProj=None): """ LeastSquaresRegularized constructor @@ -246,26 +253,26 @@ def __init__(self, model: Vector, data: Vector, op, epsilon: float, grad_mask: V self.grad = self.dmodel.clone() # Copying the pointer to data vector self.data = data - # Setting a prior domain (if any) + # Setting a prior model (if any) self.prior_model = prior_model # Setting linear operators # Assuming identity operator if regularization operator was not provided if reg_op is None: - reg_op = Identity(self.model) - # Checking if space of the prior domain is consistent with range of + reg_op = O.Identity(self.model) + # Checking if space of the prior model is consistent with range of # regularization operator if self.prior_model is not None: if not self.prior_model.checkSame(reg_op.range): - raise ValueError("Prior domain space no consistent with range of regularization operator") - self.op = Vstack(op, reg_op) # Modeling operator + raise ValueError("Prior model space no consistent with range of regularization operator") + self.op = O.Vstack(op, reg_op) # Modeling operator self.epsilon = epsilon # Regularization weight # Checking if a gradient mask was provided self.grad_mask = grad_mask if self.grad_mask is not None: if not grad_mask.checkSame(model): - raise ValueError("Mask size not consistent with domain vector!") + raise ValueError("Mask size not consistent with model vector!") self.grad_mask = grad_mask.clone() - # Residual vector (data and domain residual vectors) + # Residual vector (data and model residual vectors) self.res = self.op.range.clone() self.res.zero() # Dresidual vector @@ -277,8 +284,12 @@ def __init__(self, model: Vector, data: Vector, op, epsilon: float, grad_mask: V self.prec = prec # Objective function terms (useful to analyze each term) self.obj_terms = [None, None] + + def __del__(self): + """Default destructor""" + return - def estimate_epsilon(self, verbose: bool = False, logger=None) -> float: + def estimate_epsilon(self, verbose=False, logger=None): """ Estimate the epsilon that balances the first gradient in the 'extended-data' space or initial data residuals @@ -294,7 +305,7 @@ def estimate_epsilon(self, verbose: bool = False, logger=None) -> float: print(msg) if logger: logger.addToLog("REGULARIZED PROBLEM log file\n" + msg) - # Keeping the initial domain vector + # Keeping the initial model vector prblm_mdl = self.get_model() mdl_tmp = prblm_mdl.clone() # Keeping user-predefined epsilon if any @@ -310,18 +321,18 @@ def estimate_epsilon(self, verbose: bool = False, logger=None) -> float: # Balancing the first gradient in the 'extended-data' space prblm_res.vecs[0].scaleAdd(self.data) # Remove data vector (Lg0 - d + d) if self.prior_model is not None: - prblm_res.vecs[1].scaleAdd(self.prior_model) # Remove prior domain vector (Ag0 - m_prior + m_prior) + prblm_res.vecs[1].scaleAdd(self.prior_model) # Remove prior model vector (Ag0 - m_prior + m_prior) msg = " Epsilon balancing the data-space gradients is: %.2e" res_data_norm = prblm_res.vecs[0].norm() res_model_norm = prblm_res.vecs[1].norm() if isnan(res_model_norm) or isnan(res_data_norm): - raise ValueError("Obtained NaN: Residual-data-side-norm = %.2e, Residual-domain-side-norm = %.2e" + raise ValueError("Obtained NaN: Residual-data-side-norm = %.2e, Residual-model-side-norm = %.2e" % (res_data_norm, res_model_norm)) if res_model_norm == 0.: raise ValueError("Model residual component norm is zero, cannot find epsilon scale") # Resetting user-predefined epsilon if any self.epsilon = epsilon - # Resetting problem initial domain vector + # Resetting problem initial model vector self.set_model(mdl_tmp) del mdl_tmp epsilon_balance = res_data_norm / res_model_norm @@ -333,7 +344,7 @@ def estimate_epsilon(self, verbose: bool = False, logger=None) -> float: if logger: logger.addToLog(msg + "\nREGULARIZED PROBLEM end log file") return epsilon_balance - + def resf(self, model): r""" Method to return residual vector @@ -360,7 +371,7 @@ def resf(self, model): # Scaling by epsilon epsilon*r_m self.res.vecs[1].scale(self.epsilon) return self.res - + def gradf(self, model, res): r""" Method to return gradient vector @@ -368,39 +379,40 @@ def gradf(self, model, res): .. math:: \mathbf{g} = \mathbf{A}' \mathbf{r}_d + \varepsilon \mathbf{R}' \mathbf{r}_m """ - # Scaling by epsilon the domain residual vector (saving temporarily residual regularization) - # g = epsilon*A'r_m + # Scaling by epsilon the model residual vector (saving temporarily residual regularization) + # g = epsilon*op'r_m self.op.ops[1].adjoint(False, self.grad, res.vecs[1]) self.grad.scale(self.epsilon) - # g = L'r_d + epsilon*A'r_m + # g = L'r_d + epsilon*op'r_m self.op.ops[0].adjoint(True, self.grad, res.vecs[0]) # Applying the gradient mask if present if self.grad_mask is not None: self.grad.multiply(self.grad_mask) return self.grad - + def dresf(self, model, dmodel): r""" Method to return residual vector - + .. math:: \mathbf{d}_r = [\mathbf{A} + \varepsilon \mathbf{R}] \mathbf{d}_m """ + # Computing Ldm = dres_d self.op.forward(False, dmodel, self.dres) # Scaling by epsilon self.dres.vecs[1].scale(self.epsilon) return self.dres - + def objf(self, residual): r""" Method to return objective function value - + .. math:: \frac{1}{2} \Vert \mathbf{r}_m \Vert_2^2 + \frac{1}{2} \Vert \mathbf{r}_m \Vert_2^2 """ for idx in range(residual.n): val = residual.vecs[idx].norm() - self.obj_terms[idx] = 0.5 * val * val + self.obj_terms[idx] = 0.5 * val*val return sum(self.obj_terms) @@ -412,8 +424,8 @@ class Lasso(Problem): \frac{1}{2} \Vert \mathbf{A}\mathbf{m} - \mathbf{d}\Vert_2^2 + \lambda \Vert \mathbf{m}\Vert_1 """ - def __init__(self, model: Vector, data: Vector, op, op_norm: float = None, lambda_value: float = None, - minBound: Vector = None, maxBound: Vector = None, boundProj=None): + def __init__(self, model, data, op, op_norm=None, lambda_value=None, + minBound=None, maxBound=None, boundProj=None): """ Lasso constructor @@ -439,7 +451,7 @@ def __init__(self, model: Vector, data: Vector, op, op_norm: float = None, lambd self.data = data # Setting linear operator self.op = op # Modeling operator - # Residual vector (data and domain residual vectors) + # Residual vector (data and model residual vectors) self.res = superVector(op.range.clone(), op.domain.clone()) self.res.zero() # Dresidual vector @@ -448,7 +460,7 @@ def __init__(self, model: Vector, data: Vector, op, op_norm: float = None, lambd self.setDefaults() self.linear = True if op_norm is not None: - # Using user-provided A operator norm + # Using user-provided op operator norm self.op_norm = op_norm # Operator Norm necessary for solver else: # Evaluating operator norm using power method @@ -457,30 +469,29 @@ def __init__(self, model: Vector, data: Vector, op, op_norm: float = None, lambd # Objective function terms (useful to analyze each term) self.obj_terms = [None, None] return - + def set_lambda(self, lambda_in): - # Set lambda self.lambda_value = lambda_in return - + def objf(self, residual): r""" Method to return objective function value - + .. math:: \frac{1}{2} \Vert \mathbf{A}\mathbf{m} - \mathbf{d}\Vert_2^2 + \lambda \Vert \mathbf{m}\Vert_1 """ # data term val = residual.vecs[0].norm() - self.obj_terms[0] = 0.5 * val * val - # domain term + self.obj_terms[0] = 0.5 * val*val + # model term self.obj_terms[1] = self.lambda_value * residual.vecs[1].norm(1) return sum(self.obj_terms) - + def resf(self, model): r""" Compute the residuals from the model - + .. math:: \begin{bmatrix} \mathbf{r}_{d} \\ @@ -500,12 +511,11 @@ def resf(self, model): # Run regularization part self.res.vecs[1].copy(model) return self.res - + def dresf(self, model, dmodel): - """Linear projection of the domain perturbation onto the data space. Method not implemented""" + """Linear projection of the model perturbation onto the data space. Method not implemented""" raise NotImplementedError("dresf is not necessary for ISTC; DO NOT CALL THIS METHOD") - - # function to compute gradient (Soft thresholding applied outside in the solver) + def gradf(self, model, res): r"""Compute the gradient (the soft-thresholding is applied by the solver!) @@ -527,11 +537,11 @@ class GeneralizedLasso(Problem): \frac{1}{2} \Vert \mathbf{A}\mathbf{m} - \mathbf{d}\Vert_2^2 + \varepsilon \Vert \mathbf{Rm}\Vert_1 """ - def __init__(self, model: Vector, data: Vector, op, eps: float = 1., reg=None, - minBound: Vector = None, maxBound: Vector = None, boundProj=None): + def __init__(self, model, data, op, eps=1., reg=None, + minBound=None, maxBound=None, boundProj=None): """ GeneralizedLasso constructor - + Args: model: initial domain vector data: data vector @@ -548,15 +558,15 @@ def __init__(self, model: Vector, data: Vector, op, eps: float = 1., reg=None, self.grad = self.dmodel.clone() self.data = data self.op = op - + self.minBound = minBound self.maxBound = maxBound self.boundProj = boundProj - + # L1 Regularization - self.reg_op = reg if reg is not None else Identity(model) + self.reg_op = reg if reg is not None else O.Identity(model) self.eps = eps - + # Last settings self.obj_terms = [None] * 2 self.linear = True @@ -565,7 +575,11 @@ def __init__(self, model: Vector, data: Vector, op, eps: float = 1., reg=None, self.res_reg = self.reg_op.range.clone().zero() # this last superVector is instantiated with pointers to res_data and res_reg! self.res = superVector(self.res_data, self.res_reg) - + + def __del__(self): + """Default destructor""" + return + def objf(self, residual, eps=None): r""" Compute objective function based on the residual (super)vector @@ -576,19 +590,19 @@ def objf(self, residual, eps=None): res_data = residual.vecs[0] res_reg = residual.vecs[1] eps = eps if eps is not None else self.eps - + # data fidelity self.obj_terms[0] = .5 * res_data.norm(2) ** 2 - + # regularization penalty self.obj_terms[1] = eps * res_reg.norm(1) - + return sum(self.obj_terms) - + def resf(self, model): r""" Compute residuals from model - + .. math:: \begin{bmatrix} \mathbf{r}_{d} \\ @@ -599,17 +613,17 @@ def resf(self, model): \mathbf{R}\mathbf{m} \\ \end{bmatrix} """ - # compute data residual: A m - d + # compute data residual: Op * m - d if model.norm() != 0: - self.op.forward(False, model, self.res_data) # rd = A * m + self.op.forward(False, model, self.res_data) # rd = Op * m else: self.res_data.zero() self.res_data.scaleAdd(self.data, 1., -1.) # rd = rd - d - + # compute L1 reg residuals if model.norm() != 0. and self.reg_op is not None: self.reg_op.forward(False, model, self.res_reg) else: self.res_reg.zero() - + return self.res diff --git a/occamypy/problem/nonlinear.py b/occamypy/problem/nonlinear.py index c8addb0..2e26517 100644 --- a/occamypy/problem/nonlinear.py +++ b/occamypy/problem/nonlinear.py @@ -1,22 +1,19 @@ from math import isnan +from occamypy.vector import superVector +from occamypy import problem as P +from occamypy import operator as O -from occamypy.operator import Identity -from occamypy.operator.nonlinear import NonlinearOperator, NonlinearVstack, VarProOperator -from occamypy.problem.base import Problem -from occamypy.problem.linear import LeastSquares, LeastSquaresRegularized -from occamypy.vector.base import superVector, Vector - -class NonlinearLeastSquares(Problem): +class NonlinearLeastSquares(P.Problem): r""" Nonlinear inverse problem of the form - + .. math:: \frac{1}{2} \Vert f(\mathbf{m}) - \mathbf{d}\Vert_2^2 """ - def __init__(self, model: Vector, data: Vector, op, grad_mask: Vector = None, - minBound: Vector = None, maxBound: Vector = None, boundProj=None): + def __init__(self, model, data, op, grad_mask=None, + minBound=None, maxBound=None, boundProj=None): """ NonlinearLeastSquares constructor @@ -45,7 +42,7 @@ def __init__(self, model: Vector, data: Vector, op, grad_mask: Vector = None, # Dresidual vector self.dres = self.res.clone() # Setting non-linear and linearized operators - if isinstance(op, NonlinearOperator): + if isinstance(op, O.NonlinearOperator): self.op = op else: raise TypeError("Not provided a non-linear operator!") @@ -53,17 +50,21 @@ def __init__(self, model: Vector, data: Vector, op, grad_mask: Vector = None, self.grad_mask = grad_mask if self.grad_mask is not None: if not grad_mask.checkSame(model): - raise ValueError("Mask size not consistent with domain vector!") + raise ValueError("Mask size not consistent with model vector!") self.grad_mask = grad_mask.clone() # Setting default variables self.setDefaults() self.linear = False return + def __del__(self): + """Default destructor""" + return + def resf(self, model): r""" Method to return residual vector - + .. math:: \mathbf{r} = f(\mathbf{m}) - \mathbf{d} """ @@ -79,7 +80,7 @@ def gradf(self, model, res): .. math:: \mathbf{g} = \mathbf{F}'(\mathbf{m}) \mathbf{r} = \mathbf{F}'(\mathbf{m}) [f(\mathbf{m}) - \mathbf{d}] """ - # Setting domain point on which the F is evaluated + # Setting model point on which the F is evaluated self.op.set_background(model) # Computing F'r = g self.op.lin_op.adjoint(False, self.grad, res) @@ -91,11 +92,11 @@ def gradf(self, model, res): def dresf(self, model, dmodel): r""" Method to return residual vector - + .. math:: \mathbf{d}_r = \mathbf{G} \mathbf{d}_m """ - # Setting domain point on which the F is evaluated + # Setting model point on which the F is evaluated self.op.set_background(model) # Computing Fdm = dres self.op.lin_op.forward(False, dmodel, self.dres) @@ -104,7 +105,7 @@ def dresf(self, model, dmodel): def objf(self, residual): r""" Method to return objective function value - + .. math:: \frac{1}{2} \Vert f(\mathbf{m})-\mathbf{d}\Vert_2^2 """ @@ -113,23 +114,23 @@ def objf(self, residual): return obj -class NonlinearLeastSquaresRegularized(Problem): +class NonlinearLeastSquaresRegularized(P.Problem): r""" Nonlinear inverse problem with a linear regularization - + .. math:: \frac{1}{2} \Vert f(\mathbf{m}) - \mathbf{d} \Vert_2^2 + \frac{\varepsilon^2}{2} \Vert \mathbf{R} \mathbf{m} - \mathbf{m}_p\Vert_2^2 - + or nonlinear regularization - + .. math:: \frac{1}{2} \Vert f(\mathbf{m}) - \mathbf{d}\Vert_2^2 + \frac{\varepsilon^2}{2} \Vert g(\mathbf{m}) - \mathbf{m}_p|_2^2 """ - def __init__(self, model: Vector, data: Vector, op, epsilon: float, grad_mask: Vector=None, reg_op=None, prior_model: Vector=None, - minBound: Vector=None, maxBound: Vector=None, boundProj=None): + def __init__(self, model, data, op, epsilon, grad_mask=None, reg_op=None, prior_model=None, + minBound=None, maxBound=None, boundProj=None): """ NonlinearLeastSquaresRegularized constructor @@ -155,24 +156,24 @@ def __init__(self, model: Vector, data: Vector, op, epsilon: float, grad_mask: V self.grad = self.dmodel.clone() # Copying the pointer to data vector self.data = data - # Setting a prior domain (if any) + # Setting a prior model (if any) self.prior_model = prior_model # Setting linear operators # Assuming identity operator if regularization operator was not provided if reg_op is None: - Id_op = Identity(self.model) - reg_op = NonlinearOperator(Id_op, Id_op) - # Checking if space of the prior domain is constistent with range of regularization operator + Id_op = O.Identity(self.model) + reg_op = O.NonlinearOperator(Id_op, Id_op) + # Checking if space of the prior model is constistent with range of regularization operator if self.prior_model is not None: if not self.prior_model.checkSame(reg_op.range): - raise ValueError("Prior domain space no constistent with range of regularization operator") + raise ValueError("Prior model space no constistent with range of regularization operator") # Setting non-linear and linearized operators - if not isinstance(op, NonlinearOperator): + if not isinstance(op, O.NonlinearOperator): raise TypeError("Not provided a non-linear operator!") # Setting non-linear stack of operators - self.op = NonlinearVstack(op, reg_op) + self.op = O.NonlinearVstack(op, reg_op) self.epsilon = epsilon # Regularization weight - # Residual vector (data and domain residual vectors) + # Residual vector (data and model residual vectors) self.res = self.op.nl_op.range.clone() self.res.zero() # Dresidual vector @@ -181,7 +182,7 @@ def __init__(self, model: Vector, data: Vector, op, epsilon: float, grad_mask: V self.grad_mask = grad_mask if self.grad_mask is not None: if not grad_mask.checkSame(model): - raise ValueError("Mask size not consistent with domain vector!") + raise ValueError("Mask size not consistent with model vector!") self.grad_mask = grad_mask.clone() # Setting default variables self.setDefaults() @@ -190,14 +191,27 @@ def __init__(self, model: Vector, data: Vector, op, epsilon: float, grad_mask: V self.obj_terms = [None, None] return + def __del__(self): + """Default destructor""" + return + def estimate_epsilon(self, verbose=False, logger=None): - """Method returning epsilon that balances the two terms of the objective function""" + """ + Estimate the epsilon that balances the two terms of the objective function + + Args: + verbose: whether to print messages or not + logger: occamypy.Logger instance to log the estimate + + Returns: + estimated epsilon + """ msg = "Epsilon Scale evaluation" if verbose: print(msg) if logger: logger.addToLog("REGULARIZED PROBLEM log file\n" + msg) - # Keeping the initial domain vector + # Keeping the initial model vector prblm_mdl = self.get_model() # Keeping user-predefined epsilon if any epsilon = self.epsilon @@ -208,7 +222,7 @@ def estimate_epsilon(self, verbose=False, logger=None): res_data_norm = prblm_res.vecs[0].norm() res_model_norm = prblm_res.vecs[1].norm() if isnan(res_model_norm) or isnan(res_data_norm): - raise ValueError("Obtained NaN: Residual-data-side-norm = %s, Residual-domain-side-norm = %s" + raise ValueError("Obtained NaN: Residual-data-side-norm = %s, Residual-model-side-norm = %s" % (res_data_norm, res_model_norm)) if res_model_norm == 0.: msg = "Trying to perform a linearized step" @@ -226,11 +240,11 @@ def estimate_epsilon(self, verbose=False, logger=None): if dgrad0_dgrad0 != 0.: alpha = -dgrad0_res / dgrad0_dgrad0 else: - msg = "Cannot compute linearized alpha for the given problem! Provide a different initial domain" + msg = "Cannot compute linearized alpha for the given problem! Provide a different initial model" if logger: logger.addToLog(msg) raise ValueError(msg) - # domain=domain+alpha*grad + # model=model+alpha*grad prblm_mdl.scaleAdd(prblm_grad, 1.0, alpha) prblm_res = self.resf(prblm_mdl) # Recompute the new objective function terms @@ -238,7 +252,7 @@ def estimate_epsilon(self, verbose=False, logger=None): res_model_norm = prblm_res.vecs[1].norm() # If regularization term is still zero, stop the solver if res_model_norm == 0.: - msg = "Model residual component norm is zero, cannot find epsilon scale! Provide a different initial domain" + msg = "Model residual component norm is zero, cannot find epsilon scale! Provide a different initial model" if logger: logger.addToLog(msg) raise ValueError(msg) @@ -286,12 +300,12 @@ def gradf(self, model, res): .. math:: \mathbf{g} = \mathbf{F}' \mathbf{r}_d + \varepsilon \mathbf{G}' \mathbf{r}_m """ - # Setting domain point on which the F is evaluated + # Setting model point on which the F is evaluated self.op.set_background(model) - # g = epsilon*A'r_m + # g = epsilon*op'r_m self.op.lin_op.ops[1].adjoint(False, self.grad, res.vecs[1]) self.grad.scale(self.epsilon) - # g = F'r_d + A'(epsilon*r_m) + # g = F'r_d + op'(epsilon*r_m) self.op.lin_op.ops[0].adjoint(True, self.grad, res.vecs[0]) # Applying the gradient mask if present if self.grad_mask is not None: @@ -305,7 +319,7 @@ def dresf(self, model, dmodel): .. math:: \mathbf{d}_r = [\mathbf{F} + \varepsilon \mathbf{G}] \mathbf{d}_m """ - # Setting domain point on which the F is evaluated + # Setting model point on which the F is evaluated self.op.set_background(model) # Computing Ldm = dres_d self.op.lin_op.forward(False, dmodel, self.dres) @@ -318,20 +332,20 @@ def objf(self, residual): Method to return objective function value .. math:: - \frac{1}{2} \Vert f(\mathbf{m}) - \mathbf{d} \Vert_2^2 + - \frac{\varepsilon^2}{2} \Vert \mathbf{R} \mathbf{m} - \mathbf{m}_p\Vert_2^2 + \frac{1}{2} \Vert \mathbf{r}_d \Vert_2^2 + + \frac{\varepsilon^2}{2} \Vert \mathbf{r}_{m} \Vert_2^2 """ # data term val = residual.vecs[0].norm() self.obj_terms[0] = 0.5 * val * val - # domain term + # model term val = residual.vecs[1].norm() self.obj_terms[1] = 0.5 * val * val obj = self.obj_terms[0] + self.obj_terms[1] return obj -class VarProRegularized(Problem): +class VarProRegularized(P.Problem): r""" Non-linear inverse problem of the form @@ -347,9 +361,8 @@ class VarProRegularized(Problem): The results can only be saved on files. To the prefix specified within the lin_solver f_eval_# will be added. """ - def __init__(self, model_nl: Vector, lin_model: Vector, h_op, data: Vector, lin_solver, g_op=None, g_op_reg=None, h_op_reg=None, - data_reg=None, epsilon=None, minBound: Vector=None, maxBound: Vector=None, boundProj=None, prec=None, - warm_start: bool = False): + def __init__(self, model_nl, lin_model, h_op, data, lin_solver, g_op=None, g_op_reg=None, h_op_reg=None, + data_reg=None, epsilon=None, minBound=None, maxBound=None, boundProj=None, prec=None, warm_start=False): """ VarProRegularized constructor @@ -370,7 +383,7 @@ def __init__(self, model_nl: Vector, lin_model: Vector, h_op, data: Vector, lin_ prec: preconditioner linear operator warm_start: start VP problem from previous linearly inverted domain """ - if not isinstance(h_op, VarProOperator): + if not isinstance(h_op, O.VarProOperator): raise TypeError("ERROR! Not provided an operator class for the variable projection problem") # Setting the bounds (if any) super(VarProRegularized, self).__init__(minBound, maxBound, boundProj) @@ -378,13 +391,13 @@ def __init__(self, model_nl: Vector, lin_model: Vector, h_op, data: Vector, lin_ self.model = model_nl self.dmodel = model_nl.clone() self.dmodel.zero() - # Linear component of the inverted domain + # Linear component of the inverted model self.lin_model = lin_model self.lin_model.zero() # Copying the pointer to data vector self.data = data # Setting non-linear/linear operator - if not isinstance(h_op, VarProOperator): + if not isinstance(h_op, O.VarProOperator): raise TypeError("ERROR! Provide a VpOperator operator class for h_op") self.h_op = h_op # Setting non-linear operator (if any) @@ -406,7 +419,7 @@ def __init__(self, model_nl: Vector, lin_model: Vector, h_op, data: Vector, lin_ if self.g_op_reg is not None: res_reg = self.g_op_reg.nl_op.range.clone() elif self.h_op_reg is not None: - if not isinstance(h_op_reg, o.VarProOperator): + if not isinstance(h_op_reg, O.VarProOperator): raise TypeError("ERROR! Provide a VpOperator operator class for h_op_reg") res_reg = self.h_op_reg.h_lin.range.clone() elif self.data_reg is not None: @@ -421,11 +434,11 @@ def __init__(self, model_nl: Vector, lin_model: Vector, h_op, data: Vector, lin_ self.res = data.clone() # Instantiating linear inversion problem if self.h_op_reg is not None: - self.vp_linear_prob = LeastSquaresRegularized(self.lin_model, self.data, self.h_op.h_lin, self.epsilon, + self.vp_linear_prob = P.LeastSquaresRegularized(self.lin_model, self.data, self.h_op.h_lin, self.epsilon, reg_op=self.h_op_reg.h_lin, prior_model=self.data_reg, prec=prec) else: - self.vp_linear_prob = LeastSquares(self.lin_model, self.data, self.h_op.h_lin, prec=prec) + self.vp_linear_prob = P.LeastSquares(self.lin_model, self.data, self.h_op.h_lin, prec=prec) # Zeroing out the residual vector self.res.zero() # Dresidual vector @@ -443,8 +456,21 @@ def __init__(self, model_nl: Vector, lin_model: Vector, h_op, data: Vector, lin_ self.warm_start = warm_start return + def __del__(self): + """Default destructor""" + return + def estimate_epsilon(self, verbose=False, logger=None): - """Method returning epsilon that balances the two terms of the objective function""" + """ + Estimate the epsilon that balances the two terms of the objective function + + Args: + verbose: whether to print messages or not + logger: occamypy.Logger instance to log the estimate + + Returns: + estimated epsilon + """ if self.epsilon is None: raise ValueError("ERROR! Problem is not regularized, cannot evaluate epsilon value!") if self.g_op_reg is not None and self.h_op_reg is None: @@ -452,7 +478,7 @@ def estimate_epsilon(self, verbose=False, logger=None): msg = "Epsilon Scale evaluation" if verbose: print(msg) if logger: logger.addToLog("REGULARIZED PROBLEM log file\n" + msg) - # Keeping the initial domain vector + # Keeping the initial model vector prblm_mdl = self.get_model() # Keeping user-predefined epsilon if any epsilon = self.epsilon @@ -463,10 +489,10 @@ def estimate_epsilon(self, verbose=False, logger=None): res_data_norm = prblm_res.vecs[0].norm() res_model_norm = prblm_res.vecs[1].norm() if isnan(res_model_norm) or isnan(res_data_norm): - raise ValueError("ERROR! Obtained NaN: Residual-data-side-norm = %s, Residual-domain-side-norm = %s" % ( + raise ValueError("ERROR! Obtained NaN: Residual-data-side-norm = %s, Residual-model-side-norm = %s" % ( res_data_norm, res_model_norm)) if res_model_norm == 0.0: - msg = "Model residual component norm is zero, cannot find epsilon scale! Provide a different initial domain" + msg = "Model residual component norm is zero, cannot find epsilon scale! Provide a different initial model" if (logger): logger.addToLog(msg) raise ValueError(msg) # Resetting user-predefined epsilon if any @@ -478,7 +504,7 @@ def estimate_epsilon(self, verbose=False, logger=None): if verbose: print(msg) if logger: logger.addToLog(msg + "\nREGULARIZED PROBLEM end log file") elif self.h_op_reg is not None: - # Setting non-linear component of the domain + # Setting non-linear component of the model self.h_op.set_nl(self.model) self.h_op_reg.set_nl(self.model) # Problem is linearly regularized (fixing non-linear part and evaluating the epsilon on the linear @@ -486,6 +512,7 @@ def estimate_epsilon(self, verbose=False, logger=None): return self.vp_linear_prob.estimate_epsilon(verbose, logger) def resf(self, model): + """Method to return residual vector""" # Zero-out residual vector self.res.zero() ########################################### @@ -518,11 +545,11 @@ def resf(self, model): # Running linear inversion # Getting fevals for saving linear inversion results fevals = self.get_fevals() - # Setting initial linear inversion domain + # Setting initial linear inversion model if not self.warm_start: self.lin_model.zero() self.vp_linear_prob.set_model(self.lin_model) - # Setting non-linear component of the domain + # Setting non-linear component of the model self.h_op.set_nl(model) if self.h_op_reg is not None: self.h_op_reg.set_nl(model) @@ -542,7 +569,7 @@ def resf(self, model): if self.lin_solver.logger is not None: self.lin_solver.logger.addToLog( "#########################################################################################\n") - # Copying inverted linear optimal domain + # Copying inverted linear optimal model self.lin_model.copy(self.vp_linear_prob.get_model()) # Flushing internal saved results of the linear inversion self.lin_solver.flush_results() @@ -567,7 +594,7 @@ def gradf(self, model, res): """ # Zero-out gradient vector self.grad.zero() - # Setting the optimal linear domain component and background of the Jacobian matrices + # Setting the optimal linear model component and background of the Jacobian matrices self.h_op.set_lin_jac(self.lin_model) # H(_,m_lin_opt) self.h_op.h_nl.set_background(model) # H(m_nl,m_lin_opt) if self.h_op_reg is not None: @@ -601,9 +628,7 @@ def gradf(self, model, res): return self.grad def dresf(self, model, dmodel): - """Method to return residual vector dres (Not currently supported)""" - raise NotImplementedError( - "ERROR! dresf is not currently supported! Provide an initial step-length value different than zero.") + raise NotImplementedError("ERROR! dresf is not currently supported! Provide an initial step-length value different than zero.") def objf(self, residual): r""" @@ -617,7 +642,7 @@ def objf(self, residual): # data term val = residual.vecs[0].norm() self.obj_terms[0] = 0.5 * val * val - # domain term + # model term val = residual.vecs[1].norm() self.obj_terms[1] = 0.5 * val * val obj = self.obj_terms[0] + self.obj_terms[1] diff --git a/occamypy/solver/base.py b/occamypy/solver/base.py index 7d6bbb4..055cf62 100644 --- a/occamypy/solver/base.py +++ b/occamypy/solver/base.py @@ -1,96 +1,17 @@ +# Module containing generic Solver and Restart definitions import atexit -import datetime import os import pickle import re -from copy import deepcopy -from shutil import rmtree - import numpy as np +from shutil import rmtree +from copy import deepcopy +import datetime -from occamypy.problem.base import Problem from occamypy.utils import mkdir, sep -from occamypy.vector import Vector, VectorSet, VectorOC - +from occamypy import problem as P +from occamypy import VectorSet, VectorOC -class Restart: - """ - Restart a solver.run - - Attributes: - par_dict: dictionary containing the solver parameters - vec_dict: dictionary containing all the vectors the solver needs - restart_folder: path/to/folder where the previous run has been saved - """ - - def __init__(self): - self.par_dict = dict() - self.vec_dict = dict() - # Restart folder in case it is necessary to write restart - now = datetime.datetime.now() - restart_folder = sep.datapath + "restart_" + now.isoformat() + "/" - restart_folder = restart_folder.replace(":", "-") - self.restart_folder = restart_folder - # Calling write_restart when python session dies - atexit.register(self.write_restart) - - def save_vector(self, vec_name: str, vector_in: Vector): - """Save a vector for restarting""" - # Deleting the vector if present in the dictionary - element = self.vec_dict.pop(vec_name, None) - if element: - del element - self.vec_dict.update({vec_name: vector_in.clone()}) - - def retrieve_vector(self, vec_name: str): - """Method to retrieve a vector from restart object""" - return self.vec_dict[vec_name] - - def save_parameter(self, par_name: str, parameter_in): - """Method to save parameters for restarting""" - self.par_dict.update({par_name: parameter_in}) - return - - def retrieve_parameter(self, par_name: str): - """Method to retrieve a parameter from restart object""" - return self.par_dict[par_name] - - def write_restart(self): - """Restart destructor: it will write vectors on disk if the solver breaks""" - if bool(self.par_dict) or bool(self.vec_dict): - # Creating restarting directory - mkdir(self.restart_folder) - with open(self.restart_folder + 'restart_obj.pkl', 'wb') as out_file: - pickle.dump(self, out_file, pickle.HIGHEST_PROTOCOL) - # Checking if a vectorOC was in the restart and preventing the removal of the vector file - for vec_name, vec in self.vec_dict.items(): - if isinstance(vec, VectorOC): - vec.remove_file = False - - def read_restart(self): - """Method to read restart object from saved folder""" - if os.path.isdir(self.restart_folder): - with open(self.restart_folder + 'restart_obj.pkl', 'rb') as in_file: - restart = pickle.load(in_file) - self.par_dict = restart.par_dict - self.vec_dict = restart.vec_dict - # Checking if a vectorOC was in the restart and setting the removal of the vector file - for vec_name, vec in self.vec_dict.items(): - if isinstance(vec, VectorOC): - vec.remove_file = True - # Removing previous restart and deleting read object - restart.clear_restart() - del restart - - def clear_restart(self): - """Method to clear the restart""" - self.par_dict = dict() - self.vec_dict = dict() - # Removing restart folder if existing - if os.path.isdir(self.restart_folder): - # Removing folder - rmtree(self.restart_folder) - class Solver: """Base solver class""" @@ -130,6 +51,7 @@ def __init__(self): return def __del__(self): + """Default destructor""" return def setPrefix(self, prefix): @@ -137,9 +59,8 @@ def setPrefix(self, prefix): self.prefix = prefix return - def setDefaults(self, save_obj: bool = False, save_res: bool = False, save_grad: bool = False, - save_model: bool = False, prefix: str = None, - iter_buffer_size: int = None, iter_sampling: int = 1, flush_memory: bool = False): + def setDefaults(self, save_obj=False, save_res=False, save_grad=False, save_model=False, prefix=None, + iter_buffer_size=None, iter_sampling=1, flush_memory=False): """ Function to set parameters for result saving. @@ -161,7 +82,7 @@ def setDefaults(self, save_obj: bool = False, save_res: bool = False, save_grad: self.save_obj = save_obj # Flag to save objective function value self.save_res = save_res # Flag to save residual vector self.save_grad = save_grad # Flag to save gradient vector - self.save_model = save_model # Flag to save domain vector + self.save_model = save_model # Flag to save model vector self.flush_memory = flush_memory # Keep results in RAM or flush memory every time results are written on disk # Prefix of the saved files (if provided the results will be written on disk) @@ -174,28 +95,28 @@ def setDefaults(self, save_obj: bool = False, save_res: bool = False, save_grad: # Lists of the results (list and vector Sets) self.obj = np.array([]) # Array for objective function values self.obj_terms = np.array([]) # Array for objective function values for each terms - self.model = list() # List for domain vectors (to save results in-core) + self.model = list() # List for model vectors (to save results in-core) self.res = list() # List for residual vectors (to save results in-core) self.grad = list() # List for gradient vectors (to save results in-core) - self.modelSet = VectorSet() # Set for domain vectors + self.modelSet = VectorSet() # Set for model vectors self.resSet = VectorSet() # Set for residual vectors self.gradSet = VectorSet() # Set for gradient vectors - self.inv_model = None # Temporary saved inverted domain + self.inv_model = None # Temporary saved inverted model def flush_results(self): """Flushing internal memory of the saved results""" # Lists of the results (list and vector Sets) self.obj = np.array([]) # Array for objective function values self.obj_terms = np.array([]) # Array for objective function values for each terms - self.model = list() # List for domain vectors (to save results in-core) + self.model = list() # List for model vectors (to save results in-core) self.res = list() # List for residual vectors (to save results in-core) self.grad = list() # List for gradient vectors (to save results in-core) - self.modelSet = VectorSet() # Set for domain vectors + self.modelSet = VectorSet() # Set for model vectors self.resSet = VectorSet() # Set for residual vectors self.gradSet = VectorSet() # Set for gradient vectors - self.inv_model = None # Temporary saved inverted domain + self.inv_model = None # Temporary saved inverted model - def get_restart(self, log_file: str): + def get_restart(self, log_file): """ Function to retrieve restart folder from log file. It enables the user to use restart flag on self.run(). @@ -219,7 +140,7 @@ def get_restart(self, log_file: str): print("WARNING! No restart folder's path was found in %s" % log_file) return - def save_results(self, iiter, problem, force_save: bool = False, force_write: bool = False, **kwargs): + def save_results(self, iiter, problem, **kwargs): """ Save results to disk @@ -233,11 +154,12 @@ def save_results(self, iiter, problem, force_save: bool = False, force_write: bo obj: objective function to be saved obj_terms: if problem objective function has more than one term """ - - if not isinstance(problem, Problem): + if not isinstance(problem, P.Problem): raise TypeError("Input variable is not a Problem object") - # Getting a domain from arguments if provided (necessary to remove preconditioning) - mod_save = kwargs.get("domain", problem.get_model()) + force_save = kwargs.get("force_save", False) + force_write = kwargs.get("force_write", False) + # Getting a model from arguments if provided (necessary to remove preconditioning) + mod_save = kwargs.get("model", problem.get_model()) # Obtaining objective function value objf_value = kwargs.get("obj", problem.get_obj(problem.get_model())) obj_terms = kwargs.get("obj_terms", problem.obj_terms) if "obj_terms" in dir(problem) else None @@ -257,8 +179,8 @@ def save_results(self, iiter, problem, force_save: bool = False, force_write: bo if iiter % self.iter_sampling == 0 or force_save: if self.save_model: self.modelSet.append(mod_save) - # Storing domain vector into a temporary vector - del self.inv_model # Deallocating previous saved domain + # Storing model vector into a temporary vector + del self.inv_model # Deallocating previous saved model self.inv_model = mod_save.clone() if self.save_res: res_vec = problem.get_res(problem.get_model()) @@ -270,10 +192,10 @@ def save_results(self, iiter, problem, force_save: bool = False, force_write: bo self._write_steps(force_write) return - def _write_steps(self, force_write: bool = False): + def _write_steps(self, force_write=False): """ Method to write inversion results on disk if forced to or if buffer is filled - + Args: force_write: True - write every step; False - write whe buffer_size has been fulfilled """ @@ -281,7 +203,7 @@ def _write_steps(self, force_write: bool = False): save = True if force_write or (self.iter_buffer_size is not None and max(len(self.modelSet.vecSet), len(self.resSet.vecSet), len(self.gradSet.vecSet)) - >= self.iter_buffer_size) \ + >= self.iter_buffer_size) \ else False # Overwriting results if first time to write on disk @@ -304,12 +226,12 @@ def _write_steps(self, force_write: bool = False): # File name in which the objective function is saved obj_file = self.prefix + "_obj_comp%s.H" % (iterm + 1) sep.write_file(obj_file, self.obj_terms[:, iterm]) - # Writing current inverted domain and domain vectors on disk if requested + # Writing current inverted model and model vectors on disk if requested if self.save_model and self.prefix is not None: - inv_mod_file = self.prefix + "_inv_mod.H" # File name in which the current inverted domain is saved - model_file = self.prefix + "_model.H" # File name in which the domain vector is saved + inv_mod_file = self.prefix + "_inv_mod.H" # File name in which the current inverted model is saved + model_file = self.prefix + "_model.H" # File name in which the model vector is saved self.modelSet.writeSet(model_file, mode=mode) - self.inv_model.writeVec(inv_mod_file, mode="w") # Writing inverted domain file + self.inv_model.writeVec(inv_mod_file, mode="w") # Writing inverted model file # Writing gradient vectors on disk if requested if self.save_grad and self.prefix is not None: grad_file = self.prefix + "_gradient.H" # File name in which the gradient vector is saved @@ -319,7 +241,7 @@ def _write_steps(self, force_write: bool = False): res_file = self.prefix + "_residual.H" # File name in which the residual vector is saved self.resSet.writeSet(res_file, mode=mode) - def run(self, problem): + def run(self, prblm): """ Solve the given problem @@ -327,3 +249,83 @@ def run(self, problem): problem: problem to be solved """ raise NotImplementedError("Implement run Solver in the derived class.") + + +class Restart: + """ + Restart a solver.run + + Attributes: + par_dict: dictionary containing the solver parameters + vec_dict: dictionary containing all the vectors the solver needs + restart_folder: path/to/folder where the previous run has been saved + """ + + def __init__(self): + """Restart constructor""" + self.par_dict = dict() + self.vec_dict = dict() + # Restart folder in case it is necessary to write restart + now = datetime.datetime.now() + restart_folder = sep.datapath + "restart_" + now.isoformat() + "/" + restart_folder = restart_folder.replace(":", "-") + self.restart_folder = restart_folder + # Calling write_restart when python session dies + atexit.register(self.write_restart) + + def save_vector(self, vec_name, vector_in): + """Save vector for restarting""" + # Deleting the vector if present in the dictionary + element = self.vec_dict.pop(vec_name, None) + if element: + del element + self.vec_dict.update({vec_name: vector_in.clone()}) + + def retrieve_vector(self, vec_name): + """Method to retrieve a vector from restart object""" + return self.vec_dict[vec_name] + + def save_parameter(self, par_name, parameter_in): + """Method to save parameters for restarting""" + self.par_dict.update({par_name: parameter_in}) + return + + def retrieve_parameter(self, par_name): + """Method to retrieve a parameter from restart object""" + return self.par_dict[par_name] + + def write_restart(self): + """Restart destructor: it will write vectors on disk if the solver breaks""" + if bool(self.par_dict) or bool(self.vec_dict): + # Creating restarting directory + mkdir(self.restart_folder) + with open(self.restart_folder + 'restart_obj.pkl', 'wb') as out_file: + pickle.dump(self, out_file, pickle.HIGHEST_PROTOCOL) + # Checking if a vectorOC was in the restart and preventing the removal of the vector file + for vec_name, vec in self.vec_dict.items(): + if isinstance(vec, VectorOC): + vec.remove_file = False + + def read_restart(self): + """Method to read restart object from saved folder""" + if os.path.isdir(self.restart_folder): + with open(self.restart_folder + 'restart_obj.pkl', 'rb') as in_file: + restart = pickle.load(in_file) + self.par_dict = restart.par_dict + self.vec_dict = restart.vec_dict + # Checking if a vectorOC was in the restart and setting the removal of the vector file + for vec_name, vec in self.vec_dict.items(): + if isinstance(vec, VectorOC): + vec.remove_file = True + # Removing previous restart and deleting read object + restart.clear_restart() + del restart + + def clear_restart(self): + """Method to clear the restart""" + self.par_dict = dict() + self.vec_dict = dict() + # Removing restart folder if existing + if os.path.isdir(self.restart_folder): + # Removing folder + rmtree(self.restart_folder) diff --git a/occamypy/solver/beta_func.py b/occamypy/solver/beta_func.py deleted file mode 100644 index b0065f9..0000000 --- a/occamypy/solver/beta_func.py +++ /dev/null @@ -1,169 +0,0 @@ -# Beta functions -# grad=new gradient, grad0=old, dir=search direction -# From A SURVEY OF NONLINEAR CONJUGATE GRADIENT METHODS -def _betaFR(grad, grad0, dir, logger) -> float: - """Fletcher and Reeves method""" - # betaFR = sum(dprod(g,g))/sum(dprod(g0,g0)) - dot_grad = grad.dot(grad) - dot_grad0 = grad0.dot(grad0) - if dot_grad0 == 0.: # Avoid division by zero - beta = 0. - if logger: - logger.addToLog("Setting beta to zero since norm of previous gradient is zero!!!") - else: - beta = dot_grad / dot_grad0 - return beta - - -def _betaPRP(grad, grad0, dir, logger) -> float: - """Polak, Ribiere, Polyak method""" - # betaPRP = sum(dprod(g,g-g0))/sum(dprod(g0,g0)) - tmp1 = grad.clone() - # g-g0 - tmp1.scaleAdd(grad0, 1.0, -1.0) - dot_num = tmp1.dot(grad) - dot_grad0 = grad0.dot(grad0) - if dot_grad0 == 0.: # Avoid division by zero - beta = 0. - if logger: - logger.addToLog("Setting beta to zero since norm of previous gradient is zero!!!") - else: - beta = dot_num / dot_grad0 - return beta - - -def _betaHS(grad, grad0, dir, logger) -> float: - """Hestenes and Stiefel""" - # betaHS = sum(dprod(g,g-g0))/sum(dprod(d,g-g0)) - tmp1 = grad.clone() - # g-g0 - tmp1.scaleAdd(grad0, 1.0, -1.0) - dot_num = tmp1.dot(grad) - dot_denom = tmp1.dot(dir) - if dot_denom == 0.: # Avoid division by zero - beta = 0. - if logger: - logger.addToLog("Setting beta to zero since norm of denominator is zero!!!") - else: - beta = dot_num / dot_denom - return beta - - -def _betaCD(grad, grad0, dir, logger) -> float: - """Conjugate Descent""" - # betaCD = -sum(dprod(g,g))/sum(dprod(d,g0)) - dot_num = grad.dot(grad) - dot_denom = -grad0.dot(dir) - if dot_denom == 0.: # Avoid division by zero - beta = 0. - if logger: - logger.addToLog("Setting beta to zero since norm of denominator is zero!!!") - else: - beta = dot_num / dot_denom - return beta - - -def _betaLS(grad, grad0, dir, logger) -> float: - """Liu and Storey""" - # betaLS = -sum(dprod(g,g-g0))/sum(dprod(d,g0)) - tmp1 = grad.clone() - # g-g0 - tmp1.scaleAdd(grad0, 1.0, -1.0) - dot_num = tmp1.dot(grad) - dot_denom = -grad0.dot(dir) - if dot_denom == 0.: # Avoid division by zero - beta = 0. - if logger: - logger.addToLog("Setting beta to zero since norm of denominator is zero!!!") - else: - beta = dot_num / dot_denom - return beta - - -def _betaDY(grad, grad0, dir, logger) -> float: - """Dai and Yuan""" - # betaDY = sum(dprod(g,g))/sum(dprod(d,g-g0)) - tmp1 = grad.clone() - # g-g0 - tmp1.scaleAdd(grad0, 1.0, -1.0) - dot_num = grad.dot(grad) - dot_denom = tmp1.dot(dir) - if dot_denom == 0.: # Avoid division by zero - beta = 0. - if logger: - logger.addToLog("Setting beta to zero since norm of denominator is zero!!!") - else: - beta = dot_num / dot_denom - return beta - - -def _betaBAN(grad, grad0, dir, logger) -> float: - """Bamigbola, Ali and Nwaeze""" - # betaDY = sum(dprod(g,g-g0))/sum(dprod(g0,g-g0)) - tmp1 = grad.clone() - # g-g0 - tmp1.scaleAdd(grad0, 1.0, -1.0) - dot_num = tmp1.dot(grad) - dot_denom = tmp1.dot(grad0) - if dot_denom == 0.: # Avoid division by zero - beta = 0. - if logger: - logger.addToLog("Setting beta to zero since norm of denominator is zero!!!") - else: - beta = -dot_num / dot_denom - return beta - - -def _betaHZ(grad, grad0, dir, logger) -> float: - """Hager and Zhang""" - # betaN = sum(dprod(g-g0-2*sum(dprod(g-g0,g-g0))*d/sum(dprod(d,g-g0)),g))/sum(dprod(d,g-g0)) - tmp1 = grad.clone() - # g-g0 - tmp1.scaleAdd(grad0, 1.0, -1.0) - # sum(dprod(g-g0,g-g0)) - dot_diff_g_g0 = tmp1.dot(tmp1) - # sum(dprod(d,g-g0)) - dot_dir_diff_g_g0 = tmp1.dot(dir) - if dot_dir_diff_g_g0 == 0.: # Avoid division by zero - beta = 0. - if logger: - logger.addToLog("Setting beta to zero since norm of denominator is zero!!!") - else: - # g-g0-2*sum(dprod(g-g0,g-g0))*d/sum(dprod(d,g-g0)) - tmp1.scaleAdd(dir, 1.0, -2.0 * dot_diff_g_g0 / dot_dir_diff_g_g0) - # sum(dprod(g-g0-2*sum(dprod(g-g0,g-g0))*d/sum(dprod(d,g-g0)),g)) - dot_num = grad.dot(tmp1) - # dot_num/sum(dprod(d,g-g0)) - beta = dot_num / dot_dir_diff_g_g0 - return beta - - -def _betaSD(grad, grad0, dir, logger) -> float: - """Steepest descent""" - beta = 0. - return beta - - -def _get_beta_func(kind: str = "FR") -> callable: - kind = kind.upper() - - if kind == "FR": - return _betaFR - elif kind == "PRP": - return _betaPRP - elif kind == "HS": - return _betaHS - elif kind == "CD": - return _betaCD - elif kind == "LS": - return _betaLS - elif kind == "DY": - return _betaDY - elif kind == "BAN": - return _betaBAN - elif kind == "HZ": - return _betaHZ - elif kind == "SD": - return _betaSD - else: - raise ValueError("ERROR! Requested Beta function type not existing") \ No newline at end of file diff --git a/occamypy/solver/linear.py b/occamypy/solver/linear.py index d741451..956d901 100644 --- a/occamypy/solver/linear.py +++ b/occamypy/solver/linear.py @@ -1,18 +1,18 @@ +# Module containing Linear Solver classes from math import isnan - import numpy as np -from occamypy.problem.base import Problem -from occamypy.problem.linear import LeastSquaresSymmetric -from occamypy.solver.base import Solver -from occamypy.solver.stopper import Stopper -from occamypy.utils import ZERO, Logger +from occamypy.solver import Solver +from occamypy import problem as P + +from occamypy.utils import ZERO class CG(Solver): """Linear-Conjugate Gradient and Steepest-Descent Solver""" - - def __init__(self, stopper: Stopper, steepest: bool = False, logger: Logger = None): + + # Default class methods/functions + def __init__(self, stopper, steepest=False, logger=None): """ CG/SD constructor @@ -33,22 +33,25 @@ def __init__(self, stopper: Stopper, steepest: bool = False, logger: Logger = No self.stopper.logger = self.logger # print formatting self.iter_msg = "iter = %s, obj = %.5e, resnorm = %.2e, gradnorm = %.2e, feval = %d" - - def run(self, problem: Problem, verbose: bool = False, restart: bool = False): - """Running LCG and steepest-descent solver""" + + def __del__(self): + """Default destructor""" + return + + def run(self, problem, verbose=False, restart=False): + """Running CG/SD solver""" self.create_msg = verbose or self.logger - + # Resetting stopper before running the inversion self.stopper.reset() # Check for preconditioning precond = True if "prec" in dir(problem) and problem.prec is not None else False - + if not restart: if self.create_msg: msg = 90 * "#" + "\n" msg += "\t\t\t\tPRECONDITIONED " if precond else "\t\t\t\t" - msg += "LINEAR %s SOLVER\n" % ( - "STEEPEST-DESCENT log file" if self.steepest else "CONJUGATE GRADIENT log file") + msg += "LINEAR %s SOLVER\n" % ("STEEPEST-DESCENT log file" if self.steepest else "CONJUGATE GRADIENT log file") msg += "\tRestart folder: %s\n" % self.restart.restart_folder msg += "\tModeling Operator:\t\t%s\n" % problem.op msg += 90 * "#" + "\n" @@ -56,12 +59,12 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): print(msg.replace(" log file", "")) if self.logger: self.logger.addToLog(msg) - - # Setting internal vectors (domain and search direction vectors) + + # Setting internal vectors (model and search direction vectors) prblm_mdl = problem.get_model() cg_mdl = prblm_mdl.clone() cg_dmodl = prblm_mdl.clone().zero() - + # Other internal variables iiter = 0 else: @@ -81,19 +84,19 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): cg_dres = self.restart.retrieve_vector("cg_dres") else: dot_grad_prec_grad = self.restart.retrieve_vector("dot_grad_prec_grad") - # Setting the domain and residuals to avoid residual twice computation + # Setting the model and residuals to avoid residual twice computation problem.set_model(cg_mdl) prblm_mdl = problem.get_model() # Setting residual vector to avoid its unnecessary computation problem.set_residual(self.restart.retrieve_vector("prblm_res")) - + # Common variables unrelated to restart success = True - # Variables necessary to return inverted domain if inversion stops earlier + # Variables necessary to return inverted model if inversion stops earlier prev_mdl = prblm_mdl.clone().zero() if precond: cg_prec_grad = cg_dmodl.clone().zero() - + # Iteration loop while True: # Computing objective function @@ -124,11 +127,11 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): if prblm_grad.norm() == 0.: print("Gradient vanishes identically") break - + # Saving results self.save_results(iiter, problem, force_save=False) - prev_mdl.copy(prblm_mdl) # Keeping the previous domain - + prev_mdl.copy(prblm_mdl) # Keeping the previous model + # Computing alpha and beta coefficients if precond: # Applying preconditioning to current gradient @@ -213,7 +216,7 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): # Writing on log file if self.logger: self.logger.addToLog("Conjugate alpha,beta: " + str(alpha) + ", " + str(beta)) - + if not success: if self.create_msg: msg = "Stepper couldn't find a proper step size, will terminate solver" @@ -222,24 +225,24 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): if self.logger: self.logger.addToLog(msg) break - + if precond: # modl = modl + alpha * dmodl - cg_mdl.scaleAdd(cg_dmodl, 1.0, alpha) # Update domain + cg_mdl.scaleAdd(cg_dmodl, 1.0, alpha) # Update model else: # dmodl = alpha * grad + beta * dmodl cg_dmodl.scaleAdd(prblm_grad, beta, alpha) # update search direction # modl = modl + dmodl - cg_mdl.scaleAdd(cg_dmodl) # Update domain - + cg_mdl.scaleAdd(cg_dmodl) # Update model + # Increasing iteration counter iiter += 1 - # Setting the domain + # Setting the model problem.set_model(cg_mdl) - # Projecting domain onto the bounds (if any) + # Projecting model onto the bounds (if any) if "bounds" in dir(problem): problem.bounds.apply(cg_mdl) - + if prblm_mdl.isDifferent(cg_mdl): # Model went out of the bounds msg = "Model hit provided bounds. Projecting it onto them." @@ -260,7 +263,7 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): prblm_res = problem.get_res(cg_mdl) # New residual vector cg_dres.scaleAdd(prblm_res, -1.0, 1.0) else: - # Setting residual vector to avoid its unnecessary computation (if domain was not clipped) + # Setting residual vector to avoid its unnecessary computation (if model was not clipped) if precond: # res = res + alpha * dres prblm_res.scaleAdd(cg_dmodld, 1.0, alpha) # Update residuals @@ -270,7 +273,7 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): # res = res + dres prblm_res.scaleAdd(cg_dres) # Update residuals problem.set_residual(prblm_res) - + # Computing new objective function value obj1 = problem.get_obj(cg_mdl) if obj1 >= obj0: @@ -284,8 +287,8 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): self.logger.addToLog(msg) problem.set_model(prev_mdl) break - - # Saving current domain and previous search direction in case of restart + + # Saving current model and previous search direction in case of restart self.restart.save_parameter("iter", iiter) self.restart.save_vector("cg_mdl", cg_mdl) self.restart.save_vector("cg_dmodl", cg_dmodl) @@ -295,7 +298,7 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): else: self.restart.save_parameter("dot_grad_prec_grad", dot_grad_prec_grad) self.restart.save_vector("prblm_res", prblm_res) - + # iteration info if self.create_msg: msg = self.iter_msg % (str(iiter).zfill(self.stopper.zfill), @@ -313,8 +316,8 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): raise ValueError("Either gradient norm or objective function value NaN!") if self.stopper.run(problem, iiter, initial_obj_value, verbose): break - - # Writing last inverted domain + + # Writing last inverted model self.save_results(iiter, problem, force_save=True, force_write=True) if self.create_msg: msg = 90 * "#" + "\n" @@ -327,15 +330,15 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): self.logger.addToLog(msg) # Clear restart object self.restart.clear_restart() - + return class SD(CG): - def __init__(self, stopper: Stopper, logger: Logger = None): + def __init__(self, stopper, logger=None): super(SD, self).__init__(stopper, steepest=True, logger=logger) - - + + def _sym_ortho(a, b): """ Stable implementation of Givens rotation. @@ -370,9 +373,9 @@ def _sym_ortho(a, b): class LSQR(Solver): """ - LSQR Solver parent object following algorithm in Paige and Saunders (1982) - - Notes: + LSQR Solver parent object following algorithm in Paige and Saunders (1982) + + Notes: Find the least-squares solution to a large, sparse, linear system of equations. The function solves Ax = b or min ||b - Ax||^2 If A is symmetric, LSQR should not be used! Use SymLCGsolver @@ -381,9 +384,8 @@ class LSQR(Solver): 2. Use LSQR to solve the system ``A*dx = r0``. 3. Add the correction dx to obtain a final solution ``x = x0 + dx``. """ - - def __init__(self, stopper: Stopper, estimate_cond: bool = False, estimate_var: bool = False, - logger: Logger = None): + + def __init__(self, stopper, estimate_cond=False, estimate_var=False, logger=None): """ LSQR constructor @@ -406,18 +408,22 @@ def __init__(self, stopper: Stopper, estimate_cond: bool = False, estimate_var: self.stopper.logger = self.logger # print formatting self.iter_msg = "iter = %s, obj = %.5e, resnorm = %.2e, gradnorm = %.2e, feval = %d" - - def run(self, problem: Problem, verbose: bool = False, restart: bool = False): + + def __del__(self): + """Default destructor""" + return + + def run(self, problem, verbose=False, restart=False): """Running LSQR solver""" self.create_msg = verbose or self.logger - + # Resetting stopper before running the inversion self.stopper.reset() - + # Setting internal vectors and initial variables prblm_mdl = problem.get_model() initial_mdl = prblm_mdl.clone() - + if not restart: if self.create_msg: msg = 90 * "#" + "\n" @@ -429,10 +435,10 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): print(msg.replace("log file", "")) if self.logger: self.logger.addToLog(msg) - - # If initial domain different than zero the solver will perform the following: - # 1. Compute a residual vector ``r0 = b - A*x0``. - # 2. Use LSQR to solve the system ``A*dx = r0``. + + # If initial model different than zero the solver will perform the following: + # 1. Compute a residual vector ``r0 = b - op*x0``. + # 2. Use LSQR to solve the system ``op*dx = r0``. # 3. Add the correction dx to obtain a final solution ``x = x0 + dx``. prblm_res = problem.get_res(initial_mdl) # Initial data residuals obj0 = initial_obj_value = problem.get_obj(initial_mdl) # For relative objective function value @@ -447,20 +453,20 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): # Estimating variance or diagonal elements of the inverse if self.var: self.var = x.clone() - + # Other internal variables iiter = 0 - + # Initial inversion parameters alpha = 0. beta = u.norm() - + if beta > 0.: u.scale(1. / beta) - # A.H * u => gradient with scaled residual vector + # op.H * u => gradient with scaled residual vector problem.set_model(x) # x = 0 problem.set_residual(u) # res = u - prblm_grad = problem.get_grad(x) # g = A.H * u + prblm_grad = problem.get_grad(x) # g = op.H * u v.copy(prblm_grad) # v = g alpha = v.norm() else: @@ -468,16 +474,16 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): if alpha > 0.: v.scale(1. / alpha) w.copy(v) - + rhobar = alpha phibar = beta anorm = 0. - + # First inversion logging self.restart.save_parameter("obj_initial", initial_obj_value) # Estimating residual and gradient norms prblm_res.scale(phibar) - prblm_grad.scale(alpha * beta) + prblm_grad.scale(alpha*beta) if self.create_msg: msg = self.iter_msg % (str(iiter).zfill(self.stopper.zfill), initial_obj_value, @@ -492,7 +498,7 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): # Check if either objective function value or gradient norm is NaN if isnan(initial_obj_value) or isnan(problem.get_gnorm(x)): raise ValueError("Either gradient norm or objective function value NaN!") - + else: # Retrieving parameters and vectors to restart the solver if self.create_msg: @@ -502,7 +508,7 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): if self.logger: self.logger.addToLog(msg) self.restart.read_restart() - + # Retrieving iteration number iiter = self.restart.retrieve_parameter("iter") initial_obj_value = self.restart.retrieve_parameter("obj_initial") @@ -514,7 +520,7 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): v = self.restart.retrieve_vector("v") problem.set_model(x) problem.set_residual(u) - + prblm_res = problem.get_res(x) if self.est_cond: dk = self.restart.retrieve_vector("dk") @@ -526,47 +532,47 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): rhobar = self.restart.retrieve_parameter("rhobar") phibar = self.restart.retrieve_parameter("phibar") anorm = self.restart.retrieve_parameter("anorm") - + # Common variables unrelated to restart prblm_mdl = problem.get_model() - inv_model = prblm_mdl.clone() # Inverted domain to be saved during the inversion - # Variables necessary to return inverted domain if inversion stops earlier + inv_model = prblm_mdl.clone() # Inverted model to be saved during the inversion + # Variables necessary to return inverted model if inversion stops earlier prev_x = x.clone().zero() - early_stop = False - + early_stop =False + # Iteration loop while True: if problem.get_gnorm(prblm_mdl) == 0.: print("Gradient vanishes identically") break - + # Saving results inv_model.copy(initial_mdl) - inv_model.scaleAdd(x) # x = x0 + dx; Updating inverted domain + inv_model.scaleAdd(x) # x = x0 + dx; Updating inverted model self.save_results(iiter, problem, model=inv_model, force_save=False) # Necessary to save previous iteration prev_x.copy(x) - + """ % Perform the next step of the bidiagonalization to obtain the % next beta, u, alpha, v. These satisfy the relations - % beta*u = A*v - alpha*u, - % alpha*v = A'*u - beta*v. + % beta*u = op*v - alpha*u, + % alpha*v = op'*u - beta*v. """ - - # A.matvec(v) (i.e., projection of v onto the data space) + + # op.matvec(v) (i.e., projection of v onto the data space) v_prblm = problem.get_dres(x, v) - # u = A.matvec(v) - alpha * u + # u = op.matvec(v) - alpha * u u.scaleAdd(v_prblm, -alpha, 1.0) beta = u.norm() - + if beta > 0.: u.scale(1. / beta) anorm = np.sqrt(anorm ** 2 + alpha ** 2 + beta ** 2) problem.set_model(x) problem.set_residual(u) # res = u - prblm_grad = problem.get_grad(x) # g = A.H * u - # v = A.rmatvec(u) - beta * v + prblm_grad = problem.get_grad(x) # g = op.H * u + # v = op.rmatvec(u) - beta * v v.scaleAdd(prblm_grad, -beta, 1.) alpha = v.norm() if alpha > 0.: @@ -574,16 +580,16 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): else: problem.set_model(x) problem.set_residual(u) # res = u - + # Use a plane rotation to eliminate the subdiagonal element (beta) # of the lower-bidiagonal matrix, giving an upper-bidiagonal matrix. cs, sn, rho = _sym_ortho(rhobar, beta) - + theta = sn * alpha rhobar = -cs * alpha phi = cs * phibar phibar *= sn - + # Estimating residual and gradient norms prblm_res.scale(phibar) if prblm_grad.norm() > ZERO: @@ -592,16 +598,17 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): prblm_grad.zero() # New objective function value obj1 = problem.get_obj(prblm_mdl) - + + # Update x and w. # x = x + t1 * w x.scaleAdd(w, 1.0, phi / rho) # w = v + t2 * w w.scaleAdd(v, -theta / rho, 1.0) - + # Increasing iteration counter iiter += 1 - + # Checking new objective function value if obj1 >= obj0: if self.create_msg: @@ -614,7 +621,7 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): self.logger.addToLog(msg) early_stop = True break - + if self.est_cond: dk.copy(w).scale(1. / rho) ddnorm += dk.norm() ** 2 @@ -626,10 +633,10 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): # var = var + dk ** 2 self.var.scaleAdd(dk.clone().multiply(dk)) self.restart.save_vector("var", self.var) - + # Saving previous objective function value obj0 = obj1 - + # Saving state variables and vectors for restart self.restart.save_parameter("iter", iiter) self.restart.save_parameter("obj0", obj0) @@ -641,7 +648,7 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): self.restart.save_vector("x", x) self.restart.save_vector("w", w) self.restart.save_vector("v", v) - + # iteration info if self.create_msg: msg = self.iter_msg % (str(iiter).zfill(self.stopper.zfill), @@ -661,14 +668,14 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): raise ValueError("Either gradient norm or objective function value NaN!") if self.stopper.run(problem, iiter, initial_obj_value, verbose): break - - # Writing last inverted domain + + # Writing last inverted model inv_model.copy(initial_mdl) if early_stop: x.copy(prev_x) - inv_model.scaleAdd(x) # x = x0 + dx; Updating inverted domain + inv_model.scaleAdd(x) # x = x0 + dx; Updating inverted model self.save_results(iiter, problem, model=inv_model, force_save=True, force_write=True) - prblm_mdl.copy(inv_model) # Setting inverted domain to final one + prblm_mdl.copy(inv_model) # Setting inverted model to final one if self.create_msg: msg = 90 * "#" + "\n" msg += "\t\t\t\tLSQR SOLVER log file end\n" @@ -684,11 +691,12 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): class CGsym(Solver): """Linear-Conjugate Gradient and Steepest-Descent Solver for symmetric systems""" - - def __init__(self, stopper: Stopper, steepest=False, logger=None): + + # Default class methods/functions + def __init__(self, stopper, steepest=False, logger=None): """ CG/SD for symmetric systems constructor - + Args: stopper: Stopper to terminate the inversion steepest: whether to use the steepest-descent instead of conjugate gradient @@ -709,15 +717,19 @@ def __init__(self, stopper: Stopper, steepest=False, logger=None): # print formatting self.iter_msg = "iter = %s, obj = %.5e, resnorm = %.2e, feval = %d" return - - def run(self, problem: Problem, verbose: bool = False, restart: bool = False): + + def __del__(self): + """Default destructor""" + return + + def run(self, problem, verbose=False, restart=False): """Running LCG solver for symmetric systems""" self.create_msg = verbose or self.logger - + # Resetting stopper before running the inversion self.stopper.reset() # Checking if we are solving a linear square problem - if not isinstance(problem, LeastSquaresSymmetric): + if not isinstance(problem, P.LeastSquaresSymmetric): raise TypeError("ERROR! Provided problem object not a linear symmetric problem") # Check for preconditioning precond = False @@ -736,14 +748,14 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): print(msg.replace("log file", "")) if self.logger: self.logger.addToLog(msg) - - # Setting internal vectors (domain and search direction vectors) + + # Setting internal vectors (model and search direction vectors) prblm_mdl = problem.get_model() cg_mdl = prblm_mdl.clone() cg_dmodl = prblm_mdl.clone().zero() if precond: cg_prec_res = cg_dmodl.clone() - + # Other internal variables iiter = 0 beta = 0. @@ -762,17 +774,17 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): cg_mdl = self.restart.retrieve_vector("cg_mdl") cg_dmodl = self.restart.retrieve_vector("cg_dmodl") if precond: cg_prec_res = self.restart.retrieve_vector("cg_prec_res") - # Setting the domain and residuals to avoid residual double computation + # Setting the model and residuals to avoid residual double computation problem.set_model(cg_mdl) prblm_mdl = problem.get_model() # Setting residual vector to avoid its unnecessary computation problem.set_residual(self.restart.retrieve_vector("prblm_res")) - + # Common variables unrelated to restart success = True data_norm = problem.data.norm() prev_mdl = prblm_mdl.clone().zero() - + # Iteration loop while True: # Computing objective function @@ -794,19 +806,19 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): # Check if either objective function value or gradient norm is NaN if isnan(obj0): raise ValueError("Error! Objective function value NaN!") - + # Saving results self.save_results(iiter, problem, force_save=False) - # Copying current domain in case of early stop + # Copying current model in case of early stop prev_mdl.copy(cg_mdl) - + # Applying preconditioning to gradient (first time) if iiter == 0 and precond: problem.prec.forward(False, prblm_res, cg_prec_res) # dmodl = beta * dmodl - res cg_dmodl.scaleAdd(cg_prec_res if precond else prblm_res, beta, -1.0) # Update search direction prblm_ddmodl = problem.get_dres(cg_mdl, cg_dmodl) # Project search direction in the data space - + dot_dmodl_ddmodl = cg_dmodl.dot(prblm_ddmodl) if precond: dot_res = prblm_res.dot(cg_prec_res) # Dot product of residual and preconditioned one @@ -827,7 +839,7 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): # Writing on log file if self.logger: self.logger.addToLog(msg) - + if not success: if self.create_msg: msg = "Stepper couldn't find a proper step size, will terminate solver" @@ -836,23 +848,23 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): if self.logger: self.logger.addToLog(msg) break - + alpha = dot_res / dot_dmodl_ddmodl if self.logger: self.logger.addToLog("Alpha step length: %.2e" % alpha) - + # modl = modl + alpha * dmodl - cg_mdl.scaleAdd(cg_dmodl, sc2=alpha) # Update domain - + cg_mdl.scaleAdd(cg_dmodl, sc2=alpha) # Update model + # Increasing iteration counter iiter = iiter + 1 - # Setting the domain and residuals to avoid residual twice computation + # Setting the model and residuals to avoid residual twice computation problem.set_model(cg_mdl) - - # Projecting domain onto the bounds (if any) + + # Projecting model onto the bounds (if any) if "bounds" in dir(problem): problem.bounds.apply(cg_mdl) - + if prblm_mdl.isDifferent(cg_mdl): # Model went out of the bounds msg = "Model hit provided bounds. Projecting it onto them." @@ -874,13 +886,13 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): # dres is scaled by the inverse of the step length prblm_ddmodl.scale(1.0 / alpha) else: - # Setting residual vector to avoid its unnecessary computation (if domain was not clipped) - # res = res + alpha * dres = res + alpha * A * dmodl + # Setting residual vector to avoid its unnecessary computation (if model was not clipped) + # res = res + alpha * dres = res + alpha * op * dmodl prblm_res.scaleAdd(prblm_ddmodl, sc2=alpha) # update residuals problem.set_residual(prblm_res) if iiter == 1: problem.fevals += 1 # To correct objective function evaluation number since residuals are set - + # Computing new objective function value obj1 = problem.get_obj(cg_mdl) if precond: @@ -909,8 +921,8 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): problem.set_model(prev_mdl) break obj_old = obj0 # Saving objective function at iter-1 - - # Saving current domain and previous search direction in case of restart + + # Saving current model and previous search direction in case of restart self.restart.save_parameter("iter", iiter) self.restart.save_parameter("beta", beta) self.restart.save_parameter("obj_old", obj_old) @@ -920,7 +932,7 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): self.restart.save_vector("cg_prec_res", cg_prec_res) # Saving data space vectors self.restart.save_vector("prblm_res", prblm_res) - + # iteration info if self.create_msg: msg = self.iter_msg % (str(iiter).zfill(self.stopper.zfill), @@ -939,8 +951,8 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): raise ValueError("Error! Objective function value NaN!") if self.stopper.run(problem, iiter, verbose=verbose): break - - # Writing last inverted domain + + # Writing last inverted model self.save_results(iiter, problem, force_save=True, force_write=True) if self.create_msg: msg = 90 * "#" + "\n" diff --git a/occamypy/solver/nonlinear.py b/occamypy/solver/nonlinear.py index b4d90d7..d530578 100644 --- a/occamypy/solver/nonlinear.py +++ b/occamypy/solver/nonlinear.py @@ -1,27 +1,169 @@ from collections import deque -from copy import deepcopy from math import isnan - import numpy as np +from copy import deepcopy + +from occamypy import operator as O +from occamypy import problem as P +from occamypy import solver as S + +from occamypy.utils import ZERO + + +# Beta functions +# grad=new gradient, grad0=old, dir=search direction +# From op SURVEY OF NONLINEAR CONJUGATE GRADIENT METHODS + +def _betaFR(grad, grad0, dir, logger): + """Fletcher and Reeves method""" + # betaFR = sum(dprod(g,g))/sum(dprod(g0,g0)) + dot_grad = grad.dot(grad) + dot_grad0 = grad0.dot(grad0) + if dot_grad0 == 0.: # Avoid division by zero + beta = 0. + if logger: + logger.addToLog("Setting beta to zero since norm of previous gradient is zero!!!") + else: + beta = dot_grad / dot_grad0 + return beta + + +def _betaPRP(grad, grad0, dir, logger): + """Polak, Ribiere, Polyak method""" + # betaPRP = sum(dprod(g,g-g0))/sum(dprod(g0,g0)) + tmp1 = grad.clone() + # g-g0 + tmp1.scaleAdd(grad0, 1.0, -1.0) + dot_num = tmp1.dot(grad) + dot_grad0 = grad0.dot(grad0) + if dot_grad0 == 0.: # Avoid division by zero + beta = 0. + if logger: + logger.addToLog("Setting beta to zero since norm of previous gradient is zero!!!") + else: + beta = dot_num / dot_grad0 + return beta + + +def _betaHS(grad, grad0, dir, logger): + """Hestenes and Stiefel""" + # betaHS = sum(dprod(g,g-g0))/sum(dprod(d,g-g0)) + tmp1 = grad.clone() + # g-g0 + tmp1.scaleAdd(grad0, 1.0, -1.0) + dot_num = tmp1.dot(grad) + dot_denom = tmp1.dot(dir) + if dot_denom == 0.: # Avoid division by zero + beta = 0. + if logger: + logger.addToLog("Setting beta to zero since norm of denominator is zero!!!") + else: + beta = dot_num / dot_denom + return beta + + +def _betaCD(grad, grad0, dir, logger): + """Conjugate Descent""" + # betaCD = -sum(dprod(g,g))/sum(dprod(d,g0)) + dot_num = grad.dot(grad) + dot_denom = -grad0.dot(dir) + if dot_denom == 0.: # Avoid division by zero + beta = 0. + if logger: + logger.addToLog("Setting beta to zero since norm of denominator is zero!!!") + else: + beta = dot_num / dot_denom + return beta + + +def _betaLS(grad, grad0, dir, logger): + """Liu and Storey""" + # betaLS = -sum(dprod(g,g-g0))/sum(dprod(d,g0)) + tmp1 = grad.clone() + # g-g0 + tmp1.scaleAdd(grad0, 1.0, -1.0) + dot_num = tmp1.dot(grad) + dot_denom = -grad0.dot(dir) + if dot_denom == 0.: # Avoid division by zero + beta = 0. + if logger: + logger.addToLog("Setting beta to zero since norm of denominator is zero!!!") + else: + beta = dot_num / dot_denom + return beta + -from occamypy.operator import Operator, Scaling -from occamypy.problem import Problem, LeastSquaresSymmetric -from occamypy.solver.base import Solver -from occamypy.solver.linear import CGsym -from occamypy.solver.stepper import Stepper, CvSrchStep, StrongWolfe, ParabolicStep -from occamypy.solver.stopper import Stopper, BasicStopper -from occamypy.utils import ZERO, Logger -from occamypy.vector.base import Vector -from .beta_func import _get_beta_func +def _betaDY(grad, grad0, dir, logger): + """Dai and Yuan""" + # betaDY = sum(dprod(g,g))/sum(dprod(d,g-g0)) + tmp1 = grad.clone() + # g-g0 + tmp1.scaleAdd(grad0, 1.0, -1.0) + dot_num = grad.dot(grad) + dot_denom = tmp1.dot(dir) + if dot_denom == 0.: # Avoid division by zero + beta = 0. + if logger: + logger.addToLog("Setting beta to zero since norm of denominator is zero!!!") + else: + beta = dot_num / dot_denom + return beta -class NLCG(Solver): +def _betaBAN(grad, grad0, dir, logger): + """Bamigbola, Ali and Nwaeze""" + # betaDY = sum(dprod(g,g-g0))/sum(dprod(g0,g-g0)) + tmp1 = grad.clone() + # g-g0 + tmp1.scaleAdd(grad0, 1.0, -1.0) + dot_num = tmp1.dot(grad) + dot_denom = tmp1.dot(grad0) + if dot_denom == 0.: # Avoid division by zero + beta = 0. + if logger: + logger.addToLog("Setting beta to zero since norm of denominator is zero!!!") + else: + beta = -dot_num / dot_denom + return beta + + +def _betaHZ(grad, grad0, dir, logger): + """Hager and Zhang""" + # betaN = sum(dprod(g-g0-2*sum(dprod(g-g0,g-g0))*d/sum(dprod(d,g-g0)),g))/sum(dprod(d,g-g0)) + tmp1 = grad.clone() + # g-g0 + tmp1.scaleAdd(grad0, 1.0, -1.0) + # sum(dprod(g-g0,g-g0)) + dot_diff_g_g0 = tmp1.dot(tmp1) + # sum(dprod(d,g-g0)) + dot_dir_diff_g_g0 = tmp1.dot(dir) + if dot_dir_diff_g_g0 == 0.: # Avoid division by zero + beta = 0. + if logger: + logger.addToLog("Setting beta to zero since norm of denominator is zero!!!") + else: + # g-g0-2*sum(dprod(g-g0,g-g0))*d/sum(dprod(d,g-g0)) + tmp1.scaleAdd(dir, 1.0, -2.0 * dot_diff_g_g0 / dot_dir_diff_g_g0) + # sum(dprod(g-g0-2*sum(dprod(g-g0,g-g0))*d/sum(dprod(d,g-g0)),g)) + dot_num = grad.dot(tmp1) + # dot_num/sum(dprod(d,g-g0)) + beta = dot_num / dot_dir_diff_g_g0 + return beta + + +def _betaSD(grad, grad0, dir, logger): + """Steepest descent""" + beta = 0. + return beta + + +class NLCG(S.Solver): """Non-Linear Conjugate Gradient and Steepest-Descent Solver object""" - def __init__(self, stopper: Stopper, stepper: Stepper = ParabolicStep(), beta_type: str = "FR", logger: Logger = None): + def __init__(self, stoppr, stepper=None, beta_type="FR", logger=None): """ NLCG constructor - + Args: stopper: Stopper to terminate the inversion stepper: Stepper object to perform line-search step @@ -31,39 +173,62 @@ def __init__(self, stopper: Stopper, stepper: Stepper = ParabolicStep(), beta_ty # Calling parent construction super(NLCG, self).__init__() # Defining stopper object - self.stopper = stopper + self.stoppr = stoppr + # Defining stepper object - self.stepper = stepper + self.stepper = stepper if stepper is not None else S.ParabolicStep() + # Beta function to use during the inversion + self.beta_type = beta_type # Logger object to write on log file self.logger = logger # Overwriting logger of the Stopper object - self.stopper.logger = self.logger + self.stoppr.logger = self.logger # print formatting self.iter_msg = "iter = %s, obj = %.5e, resnorm = %.2e, gradnorm = %.2e, feval = %d, geval = %d" - - # Beta function to use during the inversion - self.beta_type = beta_type - # lambda function to return beta value - self.beta_func = lambda grad, grad0, dir: _get_beta_func(self.beta_type)(grad, grad0, dir, self.logger) + return - def run(self, problem: Problem, verbose: bool = False, restart: bool = False): - """Run NLCG solver - - Args: - problem: problem to be minimized - verbose: verbosity flag - restart: restart previously crashed inversion - """ + def __del__(self): + """Default destructor""" + return + + def beta_func(self, grad, grad0, dir): + """Beta function interface""" + beta_type = self.beta_type + if beta_type == "FR": + beta = _betaFR(grad, grad0, dir, self.logger) + elif beta_type == "PRP": + beta = _betaPRP(grad, grad0, dir, self.logger) + elif beta_type == "HS": + beta = _betaHS(grad, grad0, dir, self.logger) + elif beta_type == "CD": + beta = _betaCD(grad, grad0, dir, self.logger) + elif beta_type == "LS": + beta = _betaLS(grad, grad0, dir, self.logger) + elif beta_type == "DY": + beta = _betaDY(grad, grad0, dir, self.logger) + elif beta_type == "BAN": + beta = _betaBAN(grad, grad0, dir, self.logger) + elif beta_type == "HZ": + beta = _betaHZ(grad, grad0, dir, self.logger) + elif beta_type == "SD": + beta = _betaSD(grad, grad0, dir, self.logger) + else: + raise ValueError("ERROR! Requested Beta function type not existing") + return beta + + def run(self, problem, verbose=False, restart=False): + """Running NLCG solver""" self.create_msg = verbose or self.logger # Resetting stopper before running the inversion - self.stopper.reset() + self.stoppr.reset() if not restart: if self.create_msg: msg = 90 * "#" + "\n" - msg += "\t\t\tNON-LINEAR %s SOLVER log file\n" % ("STEEPEST-DESCENT" if self.beta_type == "SD" else "CONJUGATE GRADIENT") + msg += "\t\t\tNON-LINEAR %s SOLVER log file\n" % ( + "STEEPEST-DESCENT" if self.beta_type == "SD" else "CONJUGATE GRADIENT") msg += "\tRestart folder: %s\n" % self.restart.restart_folder if self.beta_type != "SD": msg += "\tConjugate method used: %s\n" % self.beta_type @@ -73,7 +238,7 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): if self.logger: self.logger.addToLog(msg) - # Setting internal vectors (domain, search direction, and previous gradient vectors) + # Setting internal vectors (model, search direction, and previous gradient vectors) prblm_mdl = problem.get_model() cg_mdl = prblm_mdl.clone() cg_dmodl = prblm_mdl.clone() @@ -99,7 +264,7 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): cg_mdl = self.restart.retrieve_vector("cg_mdl") cg_dmodl = self.restart.retrieve_vector("cg_dmodl") cg_grad0 = self.restart.retrieve_vector("cg_grad0") - # Setting the domain and residuals to avoid residual twice computation + # Setting the model and residuals to avoid residual twice computation problem.set_model(cg_mdl) prblm_mdl = problem.get_model() # Setting residual vector to avoid its unnecessary computation @@ -119,7 +284,7 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): self.restart.save_parameter("obj_initial", initial_obj_value) # iteration info if self.create_msg: - msg = self.iter_msg % (str(iiter).zfill(self.stopper.zfill), + msg = self.iter_msg % (str(iiter).zfill(self.stoppr.zfill), obj0, problem.get_rnorm(cg_mdl), problem.get_gnorm(cg_mdl), @@ -139,7 +304,7 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): # Saving results self.save_results(iiter, problem, force_save=False) - # Keeping current inverted domain + # Keeping current inverted model prev_mdl.copy(prblm_mdl) if iiter >= 1: @@ -186,7 +351,7 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): problem.set_model(prev_mdl) break - # Saving current domain and previous search direction in case of restart + # Saving current model and previous search direction in case of restart self.restart.save_parameter("iter", iiter) self.restart.save_parameter("alpha", alpha) self.restart.save_vector("cg_mdl", cg_mdl) @@ -196,7 +361,7 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): self.restart.save_vector("prblm_res", prblm_res) # iteration info if self.create_msg: - msg = self.iter_msg % (str(iiter).zfill(self.stopper.zfill), + msg = self.iter_msg % (str(iiter).zfill(self.stoppr.zfill), obj1, problem.get_rnorm(cg_mdl), problem.get_gnorm(cg_mdl), @@ -210,10 +375,10 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): # Check if either objective function value or gradient norm is NaN if isnan(obj1) or isnan(prblm_grad.norm()): raise ValueError("ERROR! Either gradient norm or objective function value NaN!") - if self.stopper.run(problem, iiter, initial_obj_value, verbose): + if self.stoppr.run(problem, iiter, initial_obj_value, verbose): break - # Writing last inverted domain + # Writing last inverted model self.save_results(iiter, problem, force_save=True, force_write=True) if self.create_msg: msg = 90 * "#" + "\n" @@ -231,13 +396,14 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): return -class TNewton(Solver): +class TNewton(S.Solver): """Truncated Newton/Gauss-Newton solver object""" - def __init__(self, stopper: Stopper, niter_max: int, Hessian: Operator, stepper: Stepper = CvSrchStep(), niter_min: int = None, warm_start: bool = True): + def __init__(self, stopper, niter_max, HessianOp, stepper=None, niter_min=None, warm_start=True, Newton_prefix=None, + logger=None): """ Truncated-Newton constructor - + Args: stopper: Stopper to terminate the inversion niter_max: number of iterations for solving Newton system when starting with a zero initial domain @@ -259,29 +425,28 @@ def __init__(self, stopper: Stopper, niter_max: int, Hessian: Operator, stepper: "niter_min of %d must be smaller or equal than niter_max of %d." % (niter_min, niter_max)) self.niter_min = niter_min # Defining stepper object - self.stepper = stepper + self.stepper = stepper if stepper is not None else S.CvSrchStep() # Warm starts requested? self.warm_start = warm_start # Hessian operator - if Hessian is not None: - if "set_background" not in dir(Hessian): + if HessianOp is not None: + if "set_background" not in dir(HessianOp): raise AttributeError("Hessian operator must have a set_background function.") # Setting linear solver for solving Newton system and problem class - StopLin = BasicStopper(niter=self.niter_max) - self.lin_solver = CGsym(StopLin) - self.NewtonPrblm = LeastSquaresSymmetric(Hessian.domain.clone(), Hessian.domain.clone(), Hessian) + StopLin = S.BasicStopper(niter=self.niter_max) + self.lin_solver = S.CGsym(StopLin) + self.NewtonPrblm = P.LeastSquaresSymmetric(HessianOp.domain.clone(), HessianOp.domain.clone(), HessianOp) return - def run(self, problem: Problem, verbose: bool = False, restart: bool = False): + def run(self, problem, verbose=False, restart=False): """Running Truncated Newton solver""" return -class LBFGS(Solver): - """L-BFGS (Limited-memory Broyden-Fletcher-Goldfarb-Shanno) Solver""" +class LBFGS(S.Solver): + """Limited-memory Broyden-Fletcher-Goldfarb-Shanno (L-BFGS) solver""" - def __init__(self, stopper: Stopper, stepper: Stepper = CvSrchStep(), save_alpha: bool = False, m_steps: int = None, - H0: Operator = None, logger: Logger = None, save_est: bool = False): + def __init__(self, stopper, stepper=None, save_alpha=False, m_steps=None, H0=None, logger=None, save_est=False): """ LBFGS constructor @@ -299,7 +464,7 @@ def __init__(self, stopper: Stopper, stepper: Stepper = CvSrchStep(), save_alpha # Defining stopper object self.stopper = stopper # Defining stepper object - self.stepper = stepper + self.stepper = stepper if stepper is not None else S.CvSrchStep() # Logger object to write on log file self.logger = logger # Overwriting logger of the Stopper object @@ -309,12 +474,12 @@ def __init__(self, stopper: Stopper, stepper: Stepper = CvSrchStep(), save_alpha self.H0 = H0 self.m_steps = m_steps self.save_est = save_est - self.tmp_vector = None # A copy of the domain vector will be create when the function run is invoked + self.tmp_vector = None # op copy of the model vector will be create when the function run is invoked self.iistep = 0 # necessary to re-used the estimated hessian inverse from previous runs # print formatting self.iter_msg = "iter = %s, obj = %.5e, resnorm = %.2e, gradnorm = %.2e, feval = %d, geval = %d" - def save_hessian_estimate(self, index: int, iiter: int): + def save_hessian_estimate(self, index, iiter): """Function to save current vector of estimated Hessian inverse""" # index of the step to save if self.prefix is not None and self.save_est: @@ -323,7 +488,7 @@ def save_hessian_estimate(self, index: int, iiter: int): self.step_vectors[index].writeVec(step_filename) self.grad_diff_vectors[index].writeVec(grad_diff_filename) - def check_rho(self, denom_dot: float, step_index: int, iiter: int): + def check_rho(self, denom_dot, step_index, iiter): """Function to check scaling factor of Hessian inverse estimate""" if denom_dot == 0.: if self.m_steps is not None: @@ -346,15 +511,16 @@ def check_rho(self, denom_dot: float, step_index: int, iiter: int): self.rho[step_index] = 1.0 / denom_dot else: self.rho.append(1.0 / denom_dot) - # Saving current update for inverse Hessian estimate (i.e., gradient-difference and domain-step vectors) - self.save_hessian_estimate(index=step_index, iiter=iiter) + # Saving current update for inverse Hessian estimate (i.e., gradient-difference and model-step vectors) + self.save_hessian_estimate(step_index, iiter) return + # BFGSMultiply function def BFGSMultiply(self, dmodl, grad, iiter): """Function to apply approximated inverse Hessian""" # Array containing dot-products if self.m_steps is not None: - alpha = [0.] * self.m_steps + alpha = [0.0] * self.m_steps # Handling of limited memory if iiter <= self.m_steps: initial_point = 0 @@ -394,13 +560,13 @@ def BFGSMultiply(self, dmodl, grad, iiter): # Apply left-hand series of operators for ii in lloop: # Check positivity, if not true skip the update - if self.rho[ii] > 0.: + if self.rho[ii] > 0.0: # beta=rhoiyi'r beta = self.rho[ii] * self.grad_diff_vectors[ii].dot(dmodl) dmodl.scaleAdd(self.step_vectors[ii], 1.0, alpha[ii] - beta) return - def run(self, problem: Problem, verbose: bool = False, keep_hessian: bool = False, restart: bool = False): + def run(self, problem, verbose=False, keep_hessian=False, restart=False): """ Run LBFGS solver @@ -412,7 +578,7 @@ def run(self, problem: Problem, verbose: bool = False, keep_hessian: bool = Fals """ # Resetting stopper before running the inversion self.stopper.reset() - # Getting domain vector + # Getting model vector prblm_mdl = problem.get_model() # Preliminary variables for Hessian inverse estimation if not keep_hessian or "rho" not in dir(self): @@ -441,7 +607,7 @@ def run(self, problem: Problem, verbose: bool = False, keep_hessian: bool = Fals if self.logger: self.logger.addToLog(msg) - # Setting internal vectors (domain, search direction, and previous gradient vectors) + # Setting internal vectors (model, search direction, and previous gradient vectors) bfgs_mdl = prblm_mdl.clone() bfgs_dmodl = prblm_mdl.clone() bfgs_dmodl.zero() @@ -463,7 +629,7 @@ def run(self, problem: Problem, verbose: bool = False, keep_hessian: bool = Fals bfgs_mdl = self.restart.retrieve_vector("bfgs_mdl") bfgs_dmodl = self.restart.retrieve_vector("bfgs_dmodl") bfgs_grad0 = self.restart.retrieve_vector("bfgs_grad0") - # Setting the domain and residuals to avoid residual twice computation + # Setting the model and residuals to avoid residual twice computation problem.set_model(bfgs_mdl) prblm_mdl = problem.get_model() # Setting residual vector to avoid its unnecessary computation @@ -515,7 +681,7 @@ def run(self, problem: Problem, verbose: bool = False, keep_hessian: bool = Fals # Saving results self.save_results(iiter, problem, force_save=False) - # Saving current inverted domain + # Saving current inverted model prev_mdl.copy(prblm_mdl) # Applying approximated Hessian inverse @@ -581,12 +747,12 @@ def run(self, problem: Problem, verbose: bool = False, keep_hessian: bool = Fals # rhon+1=1/yn+1'sn+1 denom_dot = self.grad_diff_vectors[step_index].dot(self.step_vectors[step_index]) # Checking rho - self.check_rho(denom_dot=denom_dot, step_index=step_index, iiter=self.iistep) + self.check_rho(denom_dot, step_index, self.iistep) # Making first step-length value Hessian guess if not provided by user if iiter == 0 and alpha != 1.0: self.restart.save_parameter("fist_alpha", alpha) - self.H0 = Scaling(bfgs_dmodl, alpha) if self.H0 is None else self.H0 * Scaling(bfgs_dmodl, alpha) + self.H0 = O.Scaling(bfgs_dmodl, alpha) if self.H0 is None else self.H0 * O.Scaling(bfgs_dmodl, alpha) if self.logger: self.logger.addToLog("First step-length value added to first Hessian inverse estimate!") self.stepper.alpha = 1.0 @@ -599,7 +765,7 @@ def run(self, problem: Problem, verbose: bool = False, keep_hessian: bool = Fals if iiter != 0 and not self.save_alpha: self.stepper.alpha = 1.0 - # Saving current domain and previous search direction in case of restart + # Saving current model and previous search direction in case of restart self.restart.save_parameter("iter", iiter) self.restart.save_parameter("alpha", alpha) self.restart.save_vector("bfgs_mdl", bfgs_mdl) @@ -631,7 +797,7 @@ def run(self, problem: Problem, verbose: bool = False, keep_hessian: bool = Fals if self.stopper.run(problem, iiter, initial_obj_value, verbose): break - # Writing last inverted domain + # Writing last inverted model self.save_results(iiter, problem, force_save=True, force_write=True) msg = 90 * "#" + "\n" if self.m_steps is not None: @@ -651,17 +817,17 @@ def run(self, problem: Problem, verbose: bool = False, keep_hessian: bool = Fals self.tmp_vector = None -class LBFGSB(Solver): +class LBFGSB(S.Solver): """ Limited-memory Broyden-Fletcher-Goldfarb-Shanno with Bounds (L-BFGS-B) Solver - + References: Implementation inspired by: https://github.com/bgranzow/L-BFGS-B.git """ - def __init__(self, stopper: Stopper, stepper: Stepper = StrongWolfe(), m_steps: float = np.inf, logger: Logger = None): + def __init__(self, stopper, stepper=None, m_steps=np.inf, logger=None): """ - L-BFGS-B constructor + LBFGSB constructor Args: stopper: Stopper to terminate the inversion @@ -674,7 +840,7 @@ def __init__(self, stopper: Stopper, stepper: Stepper = StrongWolfe(), m_steps: # Defining stopper object self.stopper = stopper # Defining stepper object - self.stepper = stepper + self.stepper = stepper if stepper is not None else S.StrongWolfe() # Logger object to write on log file self.logger = logger # Overwriting logger of the Stopper object @@ -685,7 +851,7 @@ def __init__(self, stopper: Stopper, stepper: Stepper = StrongWolfe(), m_steps: # print formatting self.iter_msg = "iter = %s, obj = %.5e, resnorm = %.2e, gradnorm = %.2e, feval = %d, geval = %d" - def get_breakpoints(self, bfgsb_dmodl: Vector, bfgsb_mdl: Vector, prblm_grad: Vector, minBound: Vector, maxBound: Vector): + def get_breakpoints(self, bfgsb_dmodl, bfgsb_mdl, prblm_grad, minBound, maxBound): """ Compute the breakpoint variables needed for the Cauchy point. @@ -728,8 +894,7 @@ def get_breakpoints(self, bfgsb_dmodl: Vector, bfgsb_mdl: Vector, prblm_grad: Ve F = np.argsort(t) return t_vec, F - def get_cauchy_point(self, bfgsb_mdl_cauchy: Vector, bfgsb_dmodl: Vector, bfgsb_mdl: Vector, prblm_grad: Vector, - minBound: Vector, maxBound: Vector, W, M, theta: float): + def get_cauchy_point(self, bfgsb_mdl_cauchy, bfgsb_dmodl, bfgsb_mdl, prblm_grad, minBound, maxBound, W, M, theta): """ Compute the generalized Cauchy point @@ -750,7 +915,7 @@ def get_cauchy_point(self, bfgsb_mdl_cauchy: Vector, bfgsb_dmodl: Vector, bfgsb_ Returns: c: initialization vector for subspace minimization """ - t_vec, F = self.get_breakpoints(bfgsb_dmodl=bfgsb_dmodl, bfgsb_mdl=bfgsb_mdl, prblm_grad=prblm_grad, minBound=minBound, maxBound=maxBound) + t_vec, F = self.get_breakpoints(bfgsb_dmodl, bfgsb_mdl, prblm_grad, minBound, maxBound) # xc = x; bfgsb_mdl_cauchy.copy(bfgsb_mdl) # Getting vector arrays @@ -810,39 +975,26 @@ def get_cauchy_point(self, bfgsb_mdl_cauchy: Vector, bfgsb_dmodl: Vector, bfgsb_ c += dt_min * p return c - def find_alpha(self, l, u, xc, du, free_vars_idx) -> float: + def find_alpha(self, l, u, xc, du, free_vars_idx): """ - Compute the alpha coefficient - - References: - Equation (5.8), Page 8 - - Args: - l: - u: - xc: - du: - free_vars_idx: - - Returns: - alpha_star: positive scaling parameter + Equation (5.8), Page 8. + :return: + :alpha_star positive scaling parameter. """ alpha_star = 1.0 n = len(free_vars_idx) for ii in range(n): idx = free_vars_idx[ii] - if du[ii] > 0.0: + if (du[ii] > 0.0): alpha_star = min(alpha_star, (u[idx] - xc[idx]) / du[ii]) else: alpha_star = min(alpha_star, (l[idx] - xc[idx]) / (du[ii] + self.epsmch)) - # TODO check this + ######## Check this! if alpha_star < 0.0: alpha_star = 0.0 return alpha_star - def subspace_minimization(self, bfgsb_dmodl: Vector, bfgsb_mdl: Vector, prblm_grad: Vector, - minBound: Vector, maxBound: Vector, bfgsb_mdl_cauchy: Vector, - c, W, M, theta: float) -> bool: + def subspace_min(self, bfgsb_dmodl, bfgsb_mdl, prblm_grad, minBound, maxBound, bfgsb_mdl_cauchy, c, W, M, theta): """ Subspace minimization for the quadratic domain over free variables. @@ -926,23 +1078,20 @@ def subspace_minimization(self, bfgsb_dmodl: Vector, bfgsb_mdl: Vector, prblm_gr def form_W(self, Y_mat, theta, S_mat): """ - - Args: - Y_mat: list containing y_k vectors - theta: theta parameters within the L-BFGS-B algorithm - S_mat: list containing s_k vectors - - Returns: - W: matrix of the L-BFGS Hessian estimate - Y: matrix containing the y_k vectors - S: matrix containing the s_k vectors + :param Y_mat: list containing y_k vectors + :param theta: theta parameters within the L-BFGS-B algorithm + :param S_mat: list containing s_k vectors + :return: + :W matrix of the L-BFGS Hessian estimate + :Y matrix containing the y_k vectors + :S matrix containing the s_k vectors """ Y = np.array([yk.getNdArray().ravel() for yk in Y_mat]).T S = np.array([sk.getNdArray().ravel() for sk in S_mat]).T W = np.concatenate((Y, theta * S), axis=1) return W, Y, S - def max_step(self, bfgsb_mdl: Vector, bfgsb_dmodl: Vector, minBound: Vector, maxBound: Vector) -> float: + def max_step(self, bfgsb_mdl, bfgsb_dmodl, minBound, maxBound): """ Method for determine maximum feasible step length @@ -951,7 +1100,7 @@ def max_step(self, bfgsb_mdl: Vector, bfgsb_dmodl: Vector, minBound: Vector, max bfgsb_dmodl: model perturbation vector (it contains the search direction) minBound: lower bound vector maxBound: upper bound vector - + Returns: max_alpha: maximum feasible step length """ @@ -977,7 +1126,7 @@ def max_step(self, bfgsb_mdl: Vector, bfgsb_dmodl: Vector, minBound: Vector, max max_alpha = d2 / (d1 + self.epsmch) return max_alpha - def run(self, problem: Problem, verbose: bool = False, restart: bool =False): + def run(self, problem, verbose=False, restart=False): """ Run L-BFGS-B solver @@ -988,7 +1137,7 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool =False): """ # Resetting stopper before running the inversion self.stopper.reset() - # Getting domain vector + # Getting model vector prblm_mdl = problem.get_model() # Obtaining bounds from problem @@ -1021,7 +1170,7 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool =False): if self.logger: self.logger.addToLog(msg) - # Setting internal vectors (domain, search direction, and previous gradient vectors) + # Setting internal vectors (model, search direction, and previous gradient vectors) bfgsb_mdl = prblm_mdl.clone() bfgsb_dmodl = prblm_mdl.clone() bfgsb_dmodl.zero() @@ -1054,7 +1203,7 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool =False): bfgsb_dmodl = self.restart.retrieve_vector("bfgsb_dmodl") bfgsb_grad0 = self.restart.retrieve_vector("bfgsb_grad0") M = self.restart.retrieve_parameter("M") - # Setting the domain and residuals to avoid residual twice computation + # Setting the model and residuals to avoid residual twice computation problem.set_model(bfgsb_mdl) prblm_mdl = problem.get_model() # Setting residual vector to avoid its unnecessary computation @@ -1107,7 +1256,7 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool =False): # Saving results self.save_results(iiter, problem, force_save=False) - # Saving current inverted domain + # Saving current inverted model prev_mdl.copy(prblm_mdl) # grad0 = grad bfgsb_grad0.copy(prblm_grad) @@ -1122,8 +1271,8 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool =False): bfgsb_dmodl.scaleAdd(bfgsb_mdl, 1.0, -1.0) self.stepper.alpha = min(1.0, 1.0 / (bfgsb_dmodl.norm() + self.epsmch)) else: - free_var = self.subspace_minimization(bfgsb_dmodl, bfgsb_mdl, prblm_grad, minBound, maxBound, bfgsb_mdl_cauchy, - c, W, M, theta) + free_var = self.subspace_min(bfgsb_dmodl, bfgsb_mdl, prblm_grad, minBound, maxBound, bfgsb_mdl_cauchy, + c, W, M, theta) if not free_var: bfgsb_dmodl.copy(bfgsb_mdl_cauchy) bfgsb_dmodl.scaleAdd(bfgsb_mdl, 1.0, -1.0) @@ -1175,7 +1324,7 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool =False): s_tmp = bfgsb_mdl.clone() s_tmp.scaleAdd(prev_mdl, 1.0, -1.0) - # Checking curvature condition of equation 3.9 in "A LIMITED MEMORY ALGORITHM FOR BOUND + # Checking curvature condition of equation 3.9 in "op LIMITED MEMORY ALGORITHM FOR BOUND # CONSTRAINED OPTIMIZATION" by Byrd et al. (1995) if y_tmp.dot(s_tmp) > self.epsmch * y_tmp.dot(y_tmp) and y_tmp.dot(s_tmp) > 0.0: msg = "Updating current Hessian estimate" @@ -1220,7 +1369,7 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool =False): # Increasing iteration counter iiter = iiter + 1 - # Saving current domain and previous search direction in case of restart + # Saving current model and previous search direction in case of restart self.restart.save_parameter("iter", iiter) self.restart.save_parameter("theta", theta) self.restart.save_parameter("M", M) @@ -1249,7 +1398,7 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool =False): if self.stopper.run(problem, iiter, initial_obj_value, verbose): break - # Writing last inverted domain + # Writing last inverted model self.save_results(iiter, problem, force_save=True, force_write=True) msg = 90 * "#" + "\n" if self.m_steps is not None: @@ -1264,10 +1413,10 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool =False): self.restart.clear_restart() -class MCMC(Solver): +class MCMC(S.Solver): """Markov chain Monte Carlo sampling algorithm""" - def __init__(self, stopper: Stopper, prop_distr: str = "u", temperature: float = None, logger: Logger = None, **kwargs): + def __init__(self, **kwargs): """ MCMC Solver/Sampler constructor @@ -1283,18 +1432,17 @@ def __init__(self, stopper: Stopper, prop_distr: str = "u", temperature: float = # Calling parent construction super(MCMC, self).__init__() # Defining stopper object - self.stopper = stopper + self.stopper = kwargs.get("stopper") # Logger object to write on log file - self.logger =logger + self.logger = kwargs.get("logger", None) # Overwriting logger of the Stopper object self.stopper.logger = self.logger - # Proposal distribution parameters - self.prop_dist = prop_distr.lower() - if self.prop_dist == "u": + self.prop_dist = kwargs.get("prop_distr") + if self.prop_dist == "Uni": self.max_step = kwargs.get("max_step") self.min_step = kwargs.get("min_step", -self.max_step) - elif self.prop_dist == "N": + elif self.prop_dist == "Gauss": self.sigma = kwargs.get("sigma") else: raise ValueError("Not supported prop_distr") @@ -1304,14 +1452,14 @@ def __init__(self, stopper: Stopper, prop_distr: str = "u", temperature: float = # Temperature Metropolis sampling algorithm (see, Monte Carlo sampling of # solutions to inverse problems by Mosegaard and Tarantola, 1995) # If not provided the likelihood is assumed to be passed to the run method - self.T = temperature + self.T = kwargs.get("T", None) # Reject sample outside of bounds or project it onto them self.reject_bound = True - def run(self, problem: Problem, verbose: bool = False, restart: bool = False): + def run(self, problem, verbose=False, restart=False): """ Running MCMC solver/sampler - + Args: problem: problem to be minimized verbose: verbosity flag @@ -1321,7 +1469,7 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): self.stopper.reset() # Checking if user is saving the sampled models if not self.save_model: - msg = "WARNING! save_model=False! Running MCMC sampling method will not save accepted domain samples!" + msg = "WARNING! save_model=False! Running MCMC sampling method will not save accepted model samples!" print(msg) if self.logger: self.logger.addToLog(msg) @@ -1352,7 +1500,7 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): mcmc_mdl_cur = self.restart.retrieve_vector("mcmc_mdl_cur") accepted = self.restart.retrieve_parameter("accepted") iiter = self.restart.retrieve_parameter("iiter") - # Setting the last accepted domain + # Setting the last accepted model problem.set_model(mcmc_mdl_cur) prblm_mdl = problem.get_model() @@ -1386,17 +1534,17 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): # Sampling loop while True: # Generate a candidate y from x according to the proposal distribution r(x_cur, x_prop) - if self.prop_dist == "u": - mcmc_dmodl.getNdArray()[:] = np.random.uniform(low=self.min_step, high=self.max_step, size=mcmc_dmodl.shape) - elif self.prop_dist == "N": - # mcmc_dmodl.getNdArray()[:] = np.random.normal(scale=self.sigma, size=mcmc_dmodl.shape) - mcmc_dmodl.randn(self.sigma**2) + if self.prop_dist == "Uni": + mcmc_dmodl.getNdArray()[:] = np.random.uniform(low=self.min_step, high=self.max_step, + size=mcmc_dmodl.shape) + elif self.prop_dist == "Gauss": + mcmc_dmodl.getNdArray()[:] = np.random.normal(scale=self.sigma, size=mcmc_dmodl.shape) # Compute a(x_cur, x_prop) mcmc_mdl_prop.copy(mcmc_mdl_cur) mcmc_mdl_prop.scaleAdd(mcmc_dmodl) - # Checking if domain parameters hit the bounds + # Checking if model parameters hit the bounds mcmc_mdl_check.copy(mcmc_mdl_prop) - # Projecting domain onto the bounds (if any) + # Projecting model onto the bounds (if any) if "bounds" in dir(problem): problem.bounds.apply(mcmc_mdl_check) if mcmc_mdl_prop.isDifferent(mcmc_mdl_check): @@ -1414,7 +1562,7 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): obj_prop = problem.get_obj(mcmc_mdl_prop) # Check if objective function value is NaN if isnan(obj_prop): - raise ValueError("objective function of proposed domain is NaN!") + raise ValueError("objective function of proposed model is NaN!") if self.T: # Using Metropolis method assuming an objective function was passed alpha = 1.0 @@ -1436,7 +1584,7 @@ def run(self, problem: Problem, verbose: bool = False, restart: bool = False): # Accept the x_prop with probability a if np.random.uniform() <= alpha: - # accepted proposed domain + # accepted proposed model mcmc_mdl_cur.copy(mcmc_mdl_prop) obj_current = deepcopy(obj_prop) obj_terms = deepcopy(problem.obj_terms) if "obj_terms" in dir(problem) else None diff --git a/occamypy/solver/sparsity.py b/occamypy/solver/sparsity.py index f8a51c8..1ccbd00 100644 --- a/occamypy/solver/sparsity.py +++ b/occamypy/solver/sparsity.py @@ -1,17 +1,16 @@ from math import isnan - import numpy as np -from occamypy.operator.base import Vstack -from occamypy.problem.linear import LeastSquares, Lasso, GeneralizedLasso -from occamypy.solver.base import Solver -from occamypy.solver.linear import CG, LSQR, SD -from occamypy.solver.stopper import Stopper, BasicStopper -from occamypy.utils import ZERO, Logger -from occamypy.vector.base import Vector, superVector +from occamypy import operator as O +from occamypy import vector as V +from occamypy import problem as P +from occamypy.solver import Solver, BasicStopper, CG, LSQR, SD +from occamypy.utils import Logger + +from occamypy.utils import ZERO -def _soft_thresh(x: Vector, thresh: float) -> Vector: +def _soft_thresh(x, thresh): r""" Soft-thresholding function: @@ -28,7 +27,7 @@ def _soft_thresh(x: Vector, thresh: float) -> Vector: return x.clone().sign() * x.clone().abs().addbias(-thresh).maximum(0.) -def _shrinkage(x: Vector, thresh: float, eps: float = 1e-10) -> Vector: +def _shrinkage(x, thresh, eps=1e-10): r""" Shrinkage function @@ -40,7 +39,7 @@ def _shrinkage(x: Vector, thresh: float, eps: float = 1e-10) -> Vector: return y * x.clone().abs().addbias([-t for t in thresh]).maximum(0.) -def _proximal_L2(x: Vector, thresh: float, eps: float = 1e-10) -> Vector: +def _proximal_L2(x, thresh, eps=1e-10): r""" Proximal operator for L2 distance @@ -58,8 +57,8 @@ def _proximal_L2(x: Vector, thresh: float, eps: float = 1e-10) -> Vector: class ISTA(Solver): """Iterative Shrikage-Thresholding Algorithm (ISTA) Solver""" - - def __init__(self, stopper: Stopper, fast: bool = False, logger: Logger = None): + + def __init__(self, stopper, fast=False, logger=None): """ ISTA constructor @@ -81,15 +80,19 @@ def __init__(self, stopper: Stopper, fast: bool = False, logger: Logger = None): # print formatting self.iter_msg = "iter = %s, obj = %.5e, resnorm = %.2e, gradnorm = %.2e, feval = %d" - def run(self, problem: Lasso, verbose: bool = False, restart: bool = False): - """Run ISTA solver""" + def __del__(self): + """Default destructor""" + return + + def run(self, problem, verbose=False, restart=False): + """Running ISTA solver""" self.create_msg = verbose or self.logger # Resetting stopper before running the inversion self.stopper.reset() # Checking if the provided problem is L1-LASSO - if not isinstance(problem, Lasso): - raise TypeError("Provided inverse problem has to be Lasso!") + if not isinstance(problem, P.Lasso): + raise TypeError("Provided inverse problem not ProblemL1Lasso!") # Checking if the regularization weight was set if problem.lambda_value is None: raise ValueError("Regularization weight (lambda_value) is not set!") @@ -108,7 +111,7 @@ def run(self, problem: Lasso, verbose: bool = False, restart: bool = False): if self.logger: self.logger.addToLog(msg) - # Setting internal vectors (domain, search direction, and previous gradient vectors) + # Setting internal vectors (model, search direction, and previous gradient vectors) prblm_mdl = problem.get_model() ista_mdl = prblm_mdl.clone() # Other parameters in case FISTA is requested @@ -136,12 +139,12 @@ def run(self, problem: Lasso, verbose: bool = False, restart: bool = False): t = self.restart.retrieve_parameter("t") # fista_mdl = self.restart.retrieve_vector("fista_mdl") - ista_mdl0 = ista_mdl.clone() # Previous domain in case stepping procedure fails + ista_mdl0 = ista_mdl.clone() # Previous model in case stepping procedure fails # Inversion loop while True: obj0 = problem.get_obj(ista_mdl) # Compute objective function value - prblm_grad = problem.get_grad(ista_mdl) # Compute the gradient g = - A' [y - Ax] + prblm_grad = problem.get_grad(ista_mdl) # Compute the gradient g = - op' [y - Ax] if iiter == 0: # Saving initial objective function value initial_obj_value = obj0 @@ -166,15 +169,15 @@ def run(self, problem: Lasso, verbose: bool = False, restart: bool = False): # Saving results self.save_results(iiter, problem, force_save=False) - ista_mdl0.copy(ista_mdl) # Saving domain before updating it + ista_mdl0.copy(ista_mdl) # Saving model before updating it - # Update domain x = x + scale_precond * A' [y - Ax] + # Update model x = x + scale_precond * op' [y - Ax] ista_mdl.scaleAdd(prblm_grad, 1.0, -1.0 / problem.op_norm) # SOFT-THRESHOLDING STEP ista_mdl.copy(_soft_thresh(ista_mdl, problem.lambda_value / problem.op_norm)) - # Projecting domain onto the bounds (if any) + # Projecting model onto the bounds (if any) if "bounds" in dir(problem): problem.bounds.apply(ista_mdl) @@ -199,7 +202,7 @@ def run(self, problem: Lasso, verbose: bool = False, restart: bool = False): ista_mdl.copy(ista_mdl0) break - # Saving current domain in case of restart and other parameters + # Saving current model in case of restart and other parameters self.restart.save_parameter("iter", iiter) self.restart.save_vector("ista_mdl", ista_mdl) if self.fast: @@ -225,7 +228,7 @@ def run(self, problem: Lasso, verbose: bool = False, restart: bool = False): if self.stopper.run(problem, iiter, initial_obj_value, verbose): break - # Writing last inverted domain + # Writing last inverted model self.save_results(iiter, problem, force_save=True, force_write=True) if self.create_msg: msg = 90 * "#" + "\n" @@ -242,8 +245,8 @@ def run(self, problem: Lasso, verbose: bool = False, restart: bool = False): class FISTA(ISTA): """Fast Iterative Shrikage-Thresholding Algorithm (F-ISTA) Solver""" - - def __init__(self, stopper: Stopper, logger: Logger = None): + + def __init__(self, stopper, logger=None): """ F-ISTA constructor @@ -257,8 +260,7 @@ def __init__(self, stopper: Stopper, logger: Logger = None): class ISTC(Solver): """Iterative Shrikage-Thresholding Algorithm with Cooling (ISTC) Solver""" - def __init__(self, stopper: Stopper, inner_it: int, cooling_start: float, cooling_end: float, - logger: Logger = None): + def __init__(self, stopper, inner_it, cooling_start, cooling_end, logger=None): """ ISTC constructor @@ -286,21 +288,25 @@ def __init__(self, stopper: Stopper, inner_it: int, cooling_start: float, coolin # cooling_start and cooling_end are numbers between 0 and 1 such that cooling_start <= cooling_end if not 0 <= cooling_start <= 1 or not 0 <= cooling_end <= 1 or cooling_end < cooling_start: raise ValueError("Cooling_start and end must be within [0,1] interval and cooling_start <= cooling_end") - self.cooling_start = cooling_start # start of cooling continuation as fraction of size of sorted array |A'y| - self.cooling_end = cooling_end # end of cooling continuation as fraction of size of sorted array |A'y| + self.cooling_start = cooling_start # start of cooling continuation as fraction of size of sorted array |op'y| + self.cooling_end = cooling_end # end of cooling continuation as fraction of size of sorted array |op'y| + + def __del__(self): + """Default destructor""" + return - def run(self, problem: Lasso, verbose: bool = False, restart: bool = False): - """Run ISTC solver""" + def run(self, problem, verbose=False, restart=False): + """Running ISTC solver""" self.create_msg = verbose or self.logger # Resetting stopper before running the inversion self.stopper.reset() # Checking if the provided problem is L1-LASSO - if not isinstance(problem, Lasso): - raise TypeError("Provided inverse problem has to be Lasso!") + if not isinstance(problem, P.Lasso): + raise TypeError("Provided inverse problem not ProblemL1Lasso!") # Computing preconditioning - scale_precond = 0.99 * np.sqrt(2) / problem.op_norm # scaling factor applied to operator A for preconditioning + scale_precond = 0.99 * np.sqrt(2) / problem.op_norm # scaling factor applied to operator op for preconditioning if not restart: if self.create_msg: msg = 90 * "#" + "\n" @@ -314,7 +320,7 @@ def run(self, problem: Lasso, verbose: bool = False, restart: bool = False): if self.logger: self.logger.addToLog(msg) - # Setting internal vectors (domain, search direction, and previous gradient vectors) + # Setting internal vectors (model, search direction, and previous gradient vectors) prblm_mdl = problem.get_model() istc_mdl = prblm_mdl.clone() @@ -324,13 +330,13 @@ def run(self, problem: Lasso, verbose: bool = False, restart: bool = False): # Other internal variables iiter = 0 # Computing cooling schedule for lambda values - # istc_mdl.scale(scale_precond) #Currently unnecessary since starting domain is zero + # istc_mdl.scale(scale_precond) #Currently unnecessary since starting model is zero prblm_grad = problem.get_grad(istc_mdl) grad_arr = np.copy(prblm_grad.getNdArray()) - grad_arr = np.abs(grad_arr.flatten()) # |A'y| and removing zero elements + grad_arr = np.abs(grad_arr.flatten()) # |op'y| and removing zero elements grad_arr = grad_arr[np.nonzero(grad_arr)] if grad_arr.size == 0: - raise ValueError("-- A'y is returning a null vector (i.e., y in the Null space of A')") + raise ValueError("-- op'y is returning a null vector (i.e., y in the Null space of op')") # Sorting the elements in descending order grad_arr.sort() grad_arr = np.flip(grad_arr, 0) @@ -362,7 +368,7 @@ def run(self, problem: Lasso, verbose: bool = False, restart: bool = False): # Common variables unrelated to restart success = True - istc_mdl0 = istc_mdl.clone() # Previous domain in case stepping procedure fails + istc_mdl0 = istc_mdl.clone() # Previous model in case stepping procedure fails istc_mdl_save = istc_mdl0 # used also to save results # Outer iteration loop @@ -392,7 +398,7 @@ def run(self, problem: Lasso, verbose: bool = False, restart: bool = False): self.restart.save_parameter("obj_initial", initial_obj_value) while inner_iter < self.inner_it: obj0 = problem.get_obj(istc_mdl) # Compute objective function value - prblm_grad = problem.get_grad(istc_mdl) # Compute the gradient g = - A' [y - Ax] + prblm_grad = problem.get_grad(istc_mdl) # Compute the gradient g = - op' [y - Ax] if inner_iter == 0: if self.create_msg: msg = self.iter_msg % (str(inner_iter).zfill(self.stopper.zfill), @@ -412,20 +418,20 @@ def run(self, problem: Lasso, verbose: bool = False, restart: bool = False): print("Gradient vanishes identically") break - # Removing preconditioning scaling factor from inverted domain + # Removing preconditioning scaling factor from inverted model istc_mdl_save.copy(istc_mdl) istc_mdl_save.scale(scale_precond) # Saving results self.save_results(iiter, problem, model=istc_mdl_save, force_save=False) - # Stepping for internal iteration domain update - istc_mdl0.copy(istc_mdl) # Saving domain before updating it - istc_mdl.scaleAdd(prblm_grad, 1.0, -scale_precond) # Update domain x = x + scale_precond * A' [y - Ax] + # Stepping for internal iteration model update + istc_mdl0.copy(istc_mdl) # Saving model before updating it + istc_mdl.scaleAdd(prblm_grad, 1.0, -scale_precond) # Update model x = x + scale_precond * op' [y - Ax] ######################################### # SOFT-THRESHOLDING STEP istc_mdl = _soft_thresh(istc_mdl, problem.lambda_value) ######################################### - # Projecting domain onto the bounds (if any) + # Projecting model onto the bounds (if any) if "bounds" in dir(problem): problem.bounds.apply(istc_mdl) @@ -445,7 +451,7 @@ def run(self, problem: Lasso, verbose: bool = False, restart: bool = False): istc_mdl.copy(istc_mdl0) break - # Saving current domain in case of restart and other parameters + # Saving current model in case of restart and other parameters self.restart.save_parameter("iter", iiter) self.restart.save_parameter("inner_iter", inner_iter) self.restart.save_vector("istc_mdl", istc_mdl) @@ -470,10 +476,10 @@ def run(self, problem: Lasso, verbose: bool = False, restart: bool = False): if self.stopper.run(problem, iiter, initial_obj_value, verbose): break - # Removing preconditioning scaling factor from inverted domain + # Removing preconditioning scaling factor from inverted model istc_mdl_save.copy(istc_mdl) istc_mdl_save.scale(scale_precond) - # Writing last inverted domain + # Writing last inverted model self.save_results(iiter, problem, model=istc_mdl_save, force_save=True, force_write=True) if self.create_msg: msg = 90 * "#" + "\n" @@ -488,12 +494,10 @@ def run(self, problem: Lasso, verbose: bool = False, restart: bool = False): class SplitBregman(Solver): - """Split-Bregman solver for GeneralizedLasso problems""" + """Split-Bregman algorithm for Generalized LASSO problems""" - # Default class methods/functions - def __init__(self, stopper: Stopper, logger: Logger = None, - niter_inner: int = 3, niter_solver: int = 5, breg_weight: float = 1., - linear_solver: str = 'CG', warm_start: bool = False, mod_tol: float = 1e-10): + def __init__(self, stopper, logger=None, niter_inner=3, niter_solver=5, breg_weight=1., linear_solver='CG', + warm_start=False, mod_tol=1e-10): """ SplitBregman constructor @@ -535,7 +539,6 @@ def __init__(self, stopper: Stopper, logger: Logger = None, raise ValueError("ERROR! Bregman update weight has to be <= 1") self.breg_weight = float(abs(breg_weight)) - linear_solver = linear_solver.upper() if linear_solver == 'CG': self.linear_solver = CG(BasicStopper(niter=self.niter_solver), logger=self.logger_lin_solv) elif linear_solver == 'SD': @@ -549,10 +552,10 @@ def __init__(self, stopper: Stopper, logger: Logger = None, # print formatting self.iter_msg = "iter = %s, obj = %.5e, df_obj = %.2e, reg_obj = %.2e, resnorm = %.2e" - def run(self, problem: GeneralizedLasso, verbose: bool = False, inner_verbose: bool = False, restart: bool = False): - """Run SplitBregman solver""" - if type(problem) != GeneralizedLasso: - raise TypeError("Input problem object has be a GeneralizedLasso") + def run(self, problem, verbose=False, inner_verbose=False, restart=False): + """Running SplitBregman solver""" + if type(problem) != P.GeneralizedLasso: + raise TypeError("Input problem object must be a GeneralizedLasso") verbose = True if inner_verbose else verbose self.create_msg = verbose or self.logger @@ -573,15 +576,15 @@ def run(self, problem: GeneralizedLasso, verbose: bool = False, inner_verbose: b self.warm_start = True sb_mdl_old = problem.model.clone() - reg_op = np.sqrt(problem.eps) * problem.reg_op # TODO can we avoid this? + reg_op = np.sqrt(problem.eps) * problem.reg_op # TODO can we avoid this? # inner problem - linear_problem = LeastSquares(model=sb_mdl.clone(), - data=superVector(problem.data, breg_d.clone()), - op=Vstack(problem.op, reg_op), - minBound=problem.minBound, - maxBound=problem.maxBound, - boundProj=problem.boundProj) + linear_problem = P.LeastSquares(model=sb_mdl.clone(), + data=V.superVector(problem.data, breg_d.clone()), + op=O.Vstack(problem.op, reg_op), + minBound=problem.minBound, + maxBound=problem.maxBound, + boundProj=problem.boundProj) if restart: self.restart.read_restart() @@ -604,7 +607,7 @@ def run(self, problem: GeneralizedLasso, verbose: bool = False, inner_verbose: b msg += "\tModeling Operator:\t%s\n" % problem.op msg += "\tInner iterations:\t%d\n" % self.niter_inner msg += "\tSolver iterations:\t%d\n" % self.niter_solver - msg += "\tL1 Regularizer op:\t%s\n" % problem.reg_op + msg += "\tL1 Regularizer op:\t%s\n" % problem.reg_op msg += "\tL1 Regularizer weight:\t%.2e\n" % problem.eps msg += "\tBregman update weight:\t%.2e\n" % self.breg_weight if self.warm_start: @@ -623,7 +626,7 @@ def run(self, problem: GeneralizedLasso, verbose: bool = False, inner_verbose: b # Main iteration loop while True: obj0 = problem.get_obj(sb_mdl) - # Saving previous domain vector + # Saving previous model vector sb_mdl_old.copy(sb_mdl) if outer_iter == 0: @@ -687,7 +690,7 @@ def run(self, problem: GeneralizedLasso, verbose: bool = False, inner_verbose: b breg_b.scaleAdd(RL1x, 1.0, self.breg_weight) breg_b.scaleAdd(breg_d, 1., -self.breg_weight) - # Update SB domain + # Update SB model sb_mdl.copy(linear_problem.model) # check objective function @@ -696,7 +699,7 @@ def run(self, problem: GeneralizedLasso, verbose: bool = False, inner_verbose: b chng_norm = sb_mdl_old.scaleAdd(sb_mdl, 1., -1.).norm() if chng_norm <= self.mod_tol * sb_mdl_norm: if self.create_msg: - msg = "Relative domain change (%.4e) norm smaller than given tolerance (%.4e)" \ + msg = "Relative model change (%.4e) norm smaller than given tolerance (%.4e)"\ % (chng_norm, self.mod_tol * sb_mdl_norm) if verbose: print(msg) @@ -724,7 +727,7 @@ def run(self, problem: GeneralizedLasso, verbose: bool = False, inner_verbose: b if self.stopper.run(problem, outer_iter, initial_obj_value, verbose): break - # writing last inverted domain + # writing last inverted model self.save_results(outer_iter, problem, force_save=True, force_write=True) # ending message and log file diff --git a/occamypy/solver/stepper.py b/occamypy/solver/stepper.py index b0128aa..0ca670d 100644 --- a/occamypy/solver/stepper.py +++ b/occamypy/solver/stepper.py @@ -1,27 +1,23 @@ -from copy import deepcopy -from math import isnan - import numpy as np - -from occamypy.problem.base import Problem -from occamypy.utils.logger import Logger -from occamypy.vector.base import Vector +from math import isnan +from copy import deepcopy class Stepper: - """base stepper class""" - + """Base stepper class""" + def __init__(self): return - + def __del__(self): + """Default destructor""" return - - def run(self, model: Vector, search_dir): - """Run stepper on the model vector""" + + def run(self, model, search_dir): + """Dummy stepper running method""" raise NotImplementedError("Implement run stepper in the derived class.") - - def estimate_initial_guess(self, problem: Problem, modl: Vector, dmodl: Vector, logger: Logger): + + def estimate_initial_guess(self, problem, modl, dmodl, logger): """Function to estimate initial step length value""" try: # Projecting search direction in the data space @@ -49,27 +45,21 @@ def estimate_initial_guess(self, problem: Problem, modl: Vector, dmodl: Vector, class CvSrchStep(Stepper): - """ + r""" Originally published by More and Thuente (1994) "Line Search Algorithms with Guaranteed Sufficient Decrease" CvSrch stepper (from Dianne o'Leary's code): - THE PURPOSE OF CVSRCH IS TO FIND A STEP WHICH SATISFIES - A SUFFICIENT DECREASE CONDITION AND A CURVATURE CONDITION. - - AT EACH STAGE THE SUBROUTINE UPDATES AN INTERVAL OF - UNCERTAINTY WITH ENDPOINTS STX AND STY. THE INTERVAL OF - UNCERTAINTY IS INITIALLY CHOSEN SO THAT IT CONTAINS A - MINIMIZER OF THE MODIFIED FUNCTION + THE PURPOSE OF CVSRCH IS TO FIND A STEP WHICH SATISFIES A SUFFICIENT DECREASE CONDITION AND A CURVATURE CONDITION. + AT EACH STAGE THE SUBROUTINE UPDATES AN INTERVAL OF UNCERTAINTY WITH ENDPOINTS STX AND STY. THE INTERVAL OF + UNCERTAINTY IS INITIALLY CHOSEN SO THAT IT CONTAINS A MINIMIZER OF THE MODIFIED FUNCTION + F(X+STP*S) - F(X) - FTOL*STP*(GRADF(X)'S). - IF A STEP IS OBTAINED FOR WHICH THE MODIFIED FUNCTION - HAS A NONPOSITIVE FUNCTION VALUE AND NONNEGATIVE DERIVATIVE, - THEN THE INTERVAL OF UNCERTAINTY IS CHOSEN SO THAT IT - CONTAINS A MINIMIZER OF F(X+STP*S). + IF A STEP IS OBTAINED FOR WHICH THE MODIFIED FUNCTION HAS A NONPOSITIVE FUNCTION VALUE AND NONNEGATIVE DERIVATIVE, + THEN THE INTERVAL OF UNCERTAINTY IS CHOSEN SO THAT IT CONTAINS A MINIMIZER OF F(X+STP*S). - THE ALGORITHM IS DESIGNED TO FIND A STEP WHICH SATISFIES - THE SUFFICIENT DECREASE CONDITION + THE ALGORITHM IS DESIGNED TO FIND A STEP WHICH SATISFIES THE SUFFICIENT DECREASE CONDITION F(X+STP*S) .LE. F(X) + FTOL*STP*(GRADF(X)'S), @@ -77,18 +67,13 @@ class CvSrchStep(Stepper): ABS(GRADF(X+STP*S)'S)) .LE. GTOL*ABS(GRADF(X)'S). - IF FTOL IS LESS THAN GTOL AND IF, FOR EXAMPLE, THE FUNCTION - IS BOUNDED BELOW, THEN THERE IS ALWAYS A STEP WHICH SATISFIES - BOTH CONDITIONS. IF NO STEP CAN BE FOUND WHICH SATISFIES BOTH - CONDITIONS, THEN THE ALGORITHM USUALLY STOPS WHEN ROUNDING - ERRORS PREVENT FURTHER PROGRESS. IN THIS CASE STP ONLY - SATISFIES THE SUFFICIENT DECREASE CONDITION. - + IF FTOL IS LESS THAN GTOL AND IF, FOR EXAMPLE, THE FUNCTION IS BOUNDED BELOW, THEN THERE IS ALWAYS A STEP WHICH SATISFIES + BOTH CONDITIONS. IF NO STEP CAN BE FOUND WHICH SATISFIES BOTH CONDITIONS, THEN THE ALGORITHM USUALLY STOPS WHEN ROUNDING + ERRORS PREVENT FURTHER PROGRESS. IN THIS CASE STP ONLY SATISFIES THE SUFFICIENT DECREASE CONDITION. """ - - def __init__(self, alpha: float = 0., xtol: float = 1e-16, ftol: float = 1e-4, gtol: float = 0.95, - alpha_min: float = 1e-20, alpha_max: float = 1e20, maxfev: int = 20, - xtrapf: float = 4., delta: float = 0.66): + + def __init__(self, alpha=0.0, xtol=1.0e-16, ftol=1.0e-4, gtol=0.95, alpha_min=1.0e-20, alpha_max=1.e20, maxfev=20, + xtrapf=4., delta=0.66): """ CvSrch stepper constructor @@ -134,58 +119,58 @@ def __init__(self, alpha: float = 0., xtol: float = 1e-16, ftol: float = 1e-4, g raise ValueError( "ERROR! alpha_max must be greater than alpha_min, current values: alpha_min=%.2e; alpha_max=%.2e" % (alpha_min, alpha_max)) - + def cstep(self, stx, fx, dx, sty, fy, dy, stp, fp, dp, brackt, stpmin, stpmax, logger): """ - Modified Cstep function (from the code by Dianne o'Leary July 1991): - The purpose of cstep is to compute a safeguarded step for - a linesearch and to update an interval of uncertainty for - a minimizer of the function. - - The parameter stx contains the step with the least function - value. The parameter stp contains the current step. It is - assumed that the derivative at stx is negative in the - direction of the step. If brackt is set true then a - minimizer has been bracketed in an interval of uncertainty - with endpoints stx and sty. - The subroutine statement is - - subroutine cstep(stx,fx,dx,sty,fy,dy,stp,fp,dp,brackt,stpmin,stpmax,info) - - where - - stx, fx, and dx are variables which specify the step, - the function, and the derivative at the best step obtained - so far. The derivative must be negative in the direction - of the step, that is, dx and stp-stx must have opposite - signs. On output these parameters are updated appropriately. - - sty, fy, and dy are variables which specify the step, - the function, and the derivative at the other endpoint of - the interval of uncertainty. On output these parameters are - updated appropriately. - - stp, fp, and dp are variables which specify the step, - the function, and the derivative at the current step. - If brackt is set true then on input stp must be - between stx and sty. On output stp is set to the new step. - - brackt is a logical variable which specifies if a minimizer - has been bracketed. If the minimizer has not been bracketed - then on input brackt must be set false. If the minimizer - is bracketed then on output brackt is set true. - - stpmin and stpmax are input variables which specify lower - and upper bounds for the step. - - info is an integer output variable set as follows: - If info = True, then the step has been computed - according to one of the five cases below. Otherwise - info = False, and this indicates improper input parameters. + Modified Cstep function (from the code by Dianne o'Leary July 1991): + The purpose of cstep is to compute a safeguarded step for + a linesearch and to update an interval of uncertainty for + a minimizer of the function. + + The parameter stx contains the step with the least function + value. The parameter stp contains the current step. It is + assumed that the derivative at stx is negative in the + direction of the step. If brackt is set true then a + minimizer has been bracketed in an interval of uncertainty + with endpoints stx and sty. + The subroutine statement is + + subroutine cstep(stx,fx,dx,sty,fy,dy,stp,fp,dp,brackt,stpmin,stpmax,info) + + where + + stx, fx, and dx are variables which specify the step, + the function, and the derivative at the best step obtained + so far. The derivative must be negative in the direction + of the step, that is, dx and stp-stx must have opposite + signs. On output these parameters are updated appropriately. + + sty, fy, and dy are variables which specify the step, + the function, and the derivative at the other endpoint of + the interval of uncertainty. On output these parameters are + updated appropriately. + + stp, fp, and dp are variables which specify the step, + the function, and the derivative at the current step. + If brackt is set true then on input stp must be + between stx and sty. On output stp is set to the new step. + + brackt is a logical variable which specifies if a minimizer + has been bracketed. If the minimizer has not been bracketed + then on input brackt must be set false. If the minimizer + is bracketed then on output brackt is set true. + + stpmin and stpmax are input variables which specify lower + and upper bounds for the step. + + info is an integer output variable set as follows: + If info = True, then the step has been computed + according to one of the five cases below. Otherwise + info = False, and this indicates improper input parameters. """ - + success = False # which is info - + # Check the input parameters for errors. if ((brackt and (stp <= np.minimum(stx, sty) or stp >= np.maximum(stx, sty))) or @@ -194,15 +179,15 @@ def cstep(self, stx, fx, dx, sty, fy, dy, stp, fp, dp, brackt, stpmin, stpmax, l if logger: logger.addToLog("\tFunction cstep could find step and update interval of uncertainty!") return stx, fx, dx, sty, fy, dy, stp, fp, dp, brackt, success - + # Determine if the derivatives have opposite sign. sgnd = dp * (dx / np.abs(dx)) - - # First case. A higher function value. + + # First case. op higher function value. # The minimum is bracketed. If the cubic step is closer # to stx than the quadratic step, the cubic step is taken, # else the average of the cubic and quadratic steps is taken. - + if fp > fx: success = True bound = True @@ -221,12 +206,12 @@ def cstep(self, stx, fx, dx, sty, fy, dy, stp, fp, dp, brackt, stpmin, stpmax, l else: stpf = stpc + (stpq - stpc) / 2.0 brackt = True - - # Second case. A lower function value and derivatives of + + # Second case. op lower function value and derivatives of # opposite sign. The minimum is bracketed. If the cubic # step is closer to stx than the quadratic (secant) step, # the cubic step is taken, else the quadratic step is taken. - + elif sgnd < 0.0: success = True bound = False @@ -245,8 +230,8 @@ def cstep(self, stx, fx, dx, sty, fy, dy, stp, fp, dp, brackt, stpmin, stpmax, l else: stpf = stpq brackt = True - - # Third case. A lower function value, derivatives of the + + # Third case. op lower function value, derivatives of the # same sign, and the magnitude of the derivative decreases. # The cubic step is only used if the cubic tends to infinity # in the direction of the step or if the minimum of the cubic @@ -254,20 +239,20 @@ def cstep(self, stx, fx, dx, sty, fy, dy, stp, fp, dp, brackt, stpmin, stpmax, l # either stpmin or stpmax. The quadratic (secant) step is also # computed and if the minimum is bracketed then the the step # closest to stx is taken, else the step farthest away is taken. - + elif np.abs(dp) < np.abs(dx): success = True bound = True theta = 3.0 * (fx - fp) / (stp - stx) + dx + dp s = np.linalg.norm([theta, dx, dp], np.inf) - + # The case gamma = 0 only arises if the cubic does not tend # to infinity in the direction of the step. - + gamma = s * np.sqrt(np.maximum(0., (theta / s) * (theta / s) - (dx / s) * (dp / s))) if stp > stx: gamma = -gamma - + p = (gamma - dp) + theta q = (gamma + (dx - dp)) + gamma r = p / q @@ -288,12 +273,12 @@ def cstep(self, stx, fx, dx, sty, fy, dy, stp, fp, dp, brackt, stpmin, stpmax, l stpf = stpc else: stpf = stpq - - # Fourth case. A lower function value, derivatives of the + + # Fourth case. op lower function value, derivatives of the # same sign, and the magnitude of the derivative does # not decrease. If the minimum is not bracketed, the step # is either stpmin or stpmax, else the cubic step is taken. - + else: success = True bound = False @@ -312,10 +297,10 @@ def cstep(self, stx, fx, dx, sty, fy, dy, stp, fp, dp, brackt, stpmin, stpmax, l stpf = stpmax else: stpf = stpmin - + # Update the interval of uncertainty. This update does not # depend on the new step or the case analysis above. - + if fp > fx: sty = stp; fy = fp; @@ -328,7 +313,7 @@ def cstep(self, stx, fx, dx, sty, fy, dy, stp, fp, dp, brackt, stpmin, stpmax, l stx = stp fx = fp dx = dp - + # Compute the new step and safeguard it. stpf = np.minimum(stpmax, stpf); stpf = np.maximum(stpmin, stpf); @@ -338,20 +323,17 @@ def cstep(self, stx, fx, dx, sty, fy, dy, stp, fp, dp, brackt, stpmin, stpmax, l stp = np.minimum(stx + self.delta * (sty - stx), stp) else: stp = np.maximum(stx + self.delta * (sty - stx), stp) - + return stx, fx, dx, sty, fy, dy, stp, fp, dp, brackt, success - + def run(self, problem, modl, dmodl, logger=None): - """Method to apply CvSrch stepper""" - # Writing to log file if any + """Running CvSrch stepper""" if logger: logger.addToLog("CVSRCH STEPPER BY STEP-LENGTH BRACKETING") logger.addToLog("xtol=%.2e ftol=%.2e gtol=%.2e alpha_min=%.2e alpha_max=%.2e maxfev=%d xtrapf=%.2e" - % ( - self.xtol, self.ftol, self.gtol, self.alpha_min, self.alpha_max, self.maxfev, - self.xtrapf)) + % (self.xtol, self.ftol, self.gtol, self.alpha_min, self.alpha_max, self.maxfev, self.xtrapf)) success = False - # Obtain objective function for provided domain + # Obtain objective function for provided model phi_init = problem.get_obj(modl) # Getting pointer to problem's gradient vector prblm_grad = problem.get_grad(modl) @@ -362,7 +344,7 @@ def run(self, problem, modl, dmodl, logger=None): return self.alpha, success # Model temporary vector model_step = modl.clone() - # Getting pointer to problem's domain vector + # Getting pointer to problem's model vector prblm_mdl = problem.get_model() # Initial step length value alpha = deepcopy(self.alpha) @@ -371,7 +353,7 @@ def run(self, problem, modl, dmodl, logger=None): alpha = self.estimate_initial_guess(problem, modl, dmodl, logger) if logger: logger.addToLog("\tinitial-steplength=%.2e" % alpha) - + # Initializing parameters p5 = 0.5 cstep_success = True @@ -381,7 +363,7 @@ def run(self, problem, modl, dmodl, logger=None): brackt = False stage1 = True dphi_test = self.ftol * dphi_init - + # The variables alphax, phix, dphix contain the values of the step, function, and directional derivative at the best step. # The variables alphay, phiy, dphiy contain the value of the step, function, and derivative at the other endpoint of the interval of uncertainty. # The variables alpha, phi_c, dphi_c contain the values of the step, function, and derivative at the current step. @@ -391,7 +373,7 @@ def run(self, problem, modl, dmodl, logger=None): alphay = 0.0 phiy = phi_init dphiy = dphi_init - + # Start testing iteration while True: # Set the minimum and maximum steps to correspond to the present interval of uncertainty. @@ -401,27 +383,26 @@ def run(self, problem, modl, dmodl, logger=None): else: alpha_int_min = alphax alpha_int_max = alpha + self.xtrapf * (alpha - alphax) - + # Force the step to be within the bounds alpha_max and alpha_min. alpha = np.maximum(alpha, self.alpha_min) alpha = np.minimum(alpha, self.alpha_max) - + # If an unusual termination is to occur then choose alpha be the lowest point obtained so far. if ((brackt and (alpha <= alpha_int_min or alpha >= alpha_int_max)) or fev >= self.maxfev - 1 or ( not cstep_success) or (brackt and alpha_int_max - alpha_int_min <= self.xtol * alpha_int_max)): if logger: - logger.addToLog( - "\tUnusual termination is to occur. Setting alpha to be the lowest point obtained so far.") + logger.addToLog("\tUnusual termination is to occur. Setting alpha to be the lowest point obtained so far.") alpha = alphax - + # Evaluate the function and gradient at alpha and compute the directional derivative. if logger: logger.addToLog("\tCurrent testing point (alpha=%.2e): m_current+alpha*dm" % alpha) model_step.copy(modl) model_step.scaleAdd(dmodl, sc2=alpha) - # Checking if domain parameters hit the bounds + # Checking if model parameters hit the bounds problem.set_model(model_step) - # Projecting domain onto the bounds (if any) + # Projecting model onto the bounds (if any) if "bounds" in dir(problem): problem.bounds.apply(model_step) if prblm_mdl.isDifferent(model_step): @@ -441,23 +422,20 @@ def run(self, problem, modl, dmodl, logger=None): prblm_grad = problem.get_grad(model_step) dphi_alpha = prblm_grad.dot(dmodl) phi_test1 = phi_init + alpha * dphi_test - + # Test for convergence if (brackt and (alpha <= alpha_int_min or alpha >= alpha_int_max)) or (not cstep_success): if logger: - logger.addToLog( - "\tRounding errors prevent further progress. There may not be a step which satisfies " - "the sufficient decrease and curvature conditions. Tolerances may be too small.") + logger.addToLog("\tRounding errors prevent further progress. There may not be a step which satisfies " + "the sufficient decrease and curvature conditions. Tolerances may be too small.") break if alpha == self.alpha_max and phi_alpha <= phi_test1 and dphi_alpha <= dphi_test: if logger: - logger.addToLog( - "\tThe step-length value is at the upper bound (alpha_max) of %.2e" % self.alpha_max) + logger.addToLog("\tThe step-length value is at the upper bound (alpha_max) of %.2e" % self.alpha_max) break if alpha == self.alpha_min and (phi_alpha > phi_test1 or dphi_alpha >= dphi_test): if logger: - logger.addToLog( - "\tThe step-length value is at the lower bound (alpha_min) of %.2e" % self.alpha_min) + logger.addToLog("\tThe step-length value is at the lower bound (alpha_min) of %.2e" % self.alpha_min) break if fev >= self.maxfev: if logger: @@ -465,8 +443,7 @@ def run(self, problem, modl, dmodl, logger=None): break if brackt and alpha_int_max - alpha_int_min <= self.xtol * alpha_int_max: if logger: - logger.addToLog( - "\tRelative width of the interval of uncertainty is at most xtol of %.2e" % self.xtol) + logger.addToLog("\tRelative width of the interval of uncertainty is at most xtol of %.2e" % self.xtol) break if phi_alpha <= phi_test1 and abs(dphi_test) <= self.gtol * (-dphi_init) and phi_alpha < phi_init: success = True @@ -476,18 +453,18 @@ def run(self, problem, modl, dmodl, logger=None): "of %.2e and objective function of %.2e (feval = %d)" % (alpha, phi_alpha, problem.get_fevals())) break - + # In the first stage we seek a step for which the modified function has a nonpositive value and # nonnegative derivative. if stage1 and (phi_alpha <= phi_test1) and (dphi_alpha >= np.minimum(self.ftol, self.gtol) * dphi_init): stage1 = False - - # A modified function is used to predict the step only if + + # op modified function is used to predict the step only if # we have not obtained a step for which the modified # function has a nonpositive function value and nonnegative # derivative, and if a lower function value has been # obtained but the decrease is not sufficient. - + if stage1 and (phi_alpha <= phix) and (phi_alpha > phi_test1): # Define the modified function and derivative values. phim = phi_alpha - alpha * dphi_test @@ -496,38 +473,38 @@ def run(self, problem, modl, dmodl, logger=None): dphim = dphi_alpha - dphi_test dphixm = dphix - dphi_test dphiym = dphiy - dphi_test - + # Call cstep to update the interval of uncertainty and to compute the new step. [alphax, phixm, dphixm, alphay, phiym, dphiym, alpha, phim, dphim, brackt, cstep_success] = self.cstep( alphax, phixm, dphixm, alphay, phiym, dphiym, alpha, phim, dphim, brackt, alpha_int_min, alpha_int_max, logger) - + # Reset the function and gradient values for phi. phix = phixm + alphax * dphi_test phiy = phiym + alphay * dphi_test dphix = dphixm + dphi_test dphiy = dphiym + dphi_test - + else: # Call cstep to update the interval of uncertainty and to compute the new step. [alphax, phix, dphix, alphay, phiy, dphiy, alpha, phi_alpha, dphi_alpha, brackt, cstep_success] = self.cstep(alphax, phix, dphix, alphay, phiy, dphiy, alpha, phi_alpha, dphi_alpha, brackt, alpha_int_min, alpha_int_max, logger) - + # Force a sufficient decrease in the size of the interval of uncertainty. if brackt: if abs(alphay - alphax) >= self.delta * width1: alpha = alphax + p5 * (alphay - alphax) width1 = width width = abs(alphay - alphax) - + # End of iteration. - + if success: - # Line search has finished, update domain + # Line search has finished, update model self.alpha = deepcopy(alpha) modl.copy(model_step) - + # Delete temporary vectors del model_step return alpha, success @@ -535,7 +512,7 @@ def run(self, problem, modl, dmodl, logger=None): class ParabolicStep(Stepper): """Parabolic Stepper class with three-point interpolation""" - + def __init__(self, c1=1.0, c2=2.0, ntry=10, alpha=0., alpha_scale_min=1.0e-10, alpha_scale_max=2000.00, shrink=0.25, eval_parab=True): """ @@ -560,22 +537,21 @@ def __init__(self, c1=1.0, c2=2.0, ntry=10, alpha=0., alpha_scale_min=1.0e-10, a np.log10(np.abs(float(np.finfo(np.float64).tiny)))) + 2) # Check for avoid Overflow or Underflow self.eval_parab = eval_parab return - + def run(self, problem, modl, dmodl, logger=None): """Method to apply parabolic stepper""" # Writing to log file if any global obj1 if logger: logger.addToLog("PARABOLIC STEPPER USING THREE-POINT INTERPOLATION") - logger.addToLog( - "c1=%.2e c2=%.2e ntry=%d steplength-scaling-min=%.2e steplength-scaling-max=%.2e shrinking-factor=%.2e" - % (self.c1, self.c2, self.ntry, self.alpha_scale_min, self.alpha_scale_max, self.shrink)) + logger.addToLog("c1=%.2e c2=%.2e ntry=%d steplength-scaling-min=%.2e steplength-scaling-max=%.2e shrinking-factor=%.2e" + % (self.c1, self.c2, self.ntry, self.alpha_scale_min, self.alpha_scale_max, self.shrink)) success = False - # Obtain objective function for provided domain + # Obtain objective function for provided model obj0 = problem.get_obj(modl) # Model temporary vector model_step = modl.clone() - # Getting pointer to problem's domain vector + # Getting pointer to problem's model vector prblm_mdl = problem.get_model() # Initial step length value alpha = deepcopy(self.alpha) @@ -607,9 +583,9 @@ def run(self, problem, modl, dmodl, logger=None): logger.addToLog("\tTesting point (c1=%.2e): m_current+c1*alpha*dm" % self.c1) model_step.copy(modl) model_step.scaleAdd(dmodl, sc2=self.c1 * alpha) - # Checking if domain parameters hit the bounds + # Checking if model parameters hit the bounds problem.set_model(model_step) - # Projecting domain onto the bounds (if any) + # Projecting model onto the bounds (if any) if "bounds" in dir(problem): problem.bounds.apply(model_step) if prblm_mdl.isDifferent(model_step): @@ -630,7 +606,7 @@ def run(self, problem, modl, dmodl, logger=None): if itry >= self.ntry: if logger: logger.addToLog("\t!!!Check problem definition or change solver!!!") - # Setting domain to current one and resetting initial step length value + # Setting model to current one and resetting initial step length value alpha = 0.0 self.alpha = 0.0 problem.set_model(modl) @@ -646,9 +622,9 @@ def run(self, problem, modl, dmodl, logger=None): logger.addToLog(msg) model_step.copy(modl) model_step.scaleAdd(dmodl, sc2=self.c2 * alpha) - # Checking if domain parameters hit the bounds + # Checking if model parameters hit the bounds problem.set_model(model_step) - # Projecting domain onto the bounds (if any) + # Projecting model onto the bounds (if any) if "bounds" in dir(problem): problem.bounds.apply(model_step) if prblm_mdl.isDifferent(model_step): @@ -669,7 +645,7 @@ def run(self, problem, modl, dmodl, logger=None): if itry >= self.ntry: if logger: logger.addToLog("\t!!!Check problem definition or change solver!!!") - # Setting domain to current one and resetting initial step length value + # Setting model to current one and resetting initial step length value alpha = 0.0 self.alpha = 0.0 problem.set_model(modl) @@ -689,36 +665,31 @@ def run(self, problem, modl, dmodl, logger=None): success = True alpha *= self.c1 if logger: - logger.addToLog("\tc1 best step-length value of: %.2e (feval = %d)" % ( - alpha, problem.get_fevals() - 1) + msg) + logger.addToLog("\tc1 best step-length value of: %.2e (feval = %d)" % (alpha, problem.get_fevals() - 1) + msg) break elif obj2 < obj0 and obj2 < obj1 and obj2 < obj3: success = True alpha *= self.c2 if logger: - logger.addToLog( - "\tc2 best step-length value of: %.2e (feval = %d)" % (alpha, problem.get_fevals()) + msg) + logger.addToLog("\tc2 best step-length value of: %.2e (feval = %d)" % (alpha, problem.get_fevals()) + msg) break # If points lay on a horizontal line pick minimum alpha set by user if obj0 == obj1 == obj2 or (self.c2 * (obj1 - obj0) + self.c1 * (obj0 - obj2)) == 0.: step_scale = self.alpha_scale_min if logger: - logger.addToLog( - "\tTwo testing points on a line: cannot fit a parabola, using minimum step-length of %.2e" - % (step_scale * alpha)) + logger.addToLog("\tTwo testing points on a line: cannot fit a parabola, using minimum step-length of %.2e" + % (step_scale * alpha)) else: # Otherwise, find the optimal parabolic step length step_scale = 0.5 * (self.c2 * self.c2 * (obj1 - obj0) + self.c1 * self.c1 * (obj0 - obj2)) / ( self.c2 * (obj1 - obj0) + self.c1 * (obj0 - obj2)) if logger: - logger.addToLog( - "\tTesting point (c_opt=%.2e): m_current+c_opt*alpha*dm (parabola minimum)" % step_scale) + logger.addToLog("\tTesting point (c_opt=%.2e): m_current+c_opt*alpha*dm (parabola minimum)" % step_scale) # If step length negative, re-evaluate points if step_scale < 0.: if logger: - logger.addToLog( - "\tEncountered a negative step-length value: %.2e; Setting parabola-minimum objective function to infinity." - % (step_scale * alpha)) + logger.addToLog("\tEncountered a negative step-length value: %.2e; Setting parabola-minimum objective function to infinity." + % (step_scale * alpha)) # Skipping parabola minimum and setting obj3 to infinity obj3 = np.inf else: @@ -726,23 +697,21 @@ def run(self, problem, modl, dmodl, logger=None): if step_scale < self.alpha_scale_min: if logger: logger.addToLog("\t!!! step-length scale of %.2e smaller than provided lower bound." - "Clipping its value to bound value of %.2e !!!" % ( - step_scale, self.alpha_scale_min)) + "Clipping its value to bound value of %.2e !!!" % (step_scale, self.alpha_scale_min)) step_scale = self.alpha_scale_min elif step_scale > self.alpha_scale_max: if logger: logger.addToLog("\t!!! step-length scale of %.2e greater than provided upper bound." - "Clipping its value to bound value of %.2e !!!" % ( - step_scale, self.alpha_scale_max)) + "Clipping its value to bound value of %.2e !!!" % (step_scale, self.alpha_scale_max)) step_scale = self.alpha_scale_max - + # Testing parabolic scale # Compute new objective function at the minimum of the parabolic approximation model_step.copy(modl) model_step.scaleAdd(dmodl, sc2=step_scale * alpha) - # Checking if domain parameters hit the bounds + # Checking if model parameters hit the bounds problem.set_model(model_step) - # Projecting domain onto the bounds (if any) + # Projecting model onto the bounds (if any) if "bounds" in dir(problem): problem.bounds.apply(model_step) if prblm_mdl.isDifferent(model_step): @@ -753,7 +722,7 @@ def run(self, problem, modl, dmodl, logger=None): obj3 = problem.get_obj(model_step) if logger: logger.addToLog("\tObjective function value of %.5e" % obj3) - + # Writing info to log file if logger: logger.addToLog("\tInitial objective function value: %.5e," @@ -762,43 +731,40 @@ def run(self, problem, modl, dmodl, logger=None): "Objective function at parabola minimum: %.5e" % (obj0, obj1, obj2, obj3)) itry += 1 - + # Check which one is the best step length if obj1 < obj0 and obj1 < obj2 and obj1 < obj3: success = True alpha *= self.c1 if logger: - logger.addToLog( - "\tc1 best step-length value of: %.2e (feval = %d)" % (alpha, problem.get_fevals() - 2)) + logger.addToLog("\tc1 best step-length value of: %.2e (feval = %d)" % (alpha, problem.get_fevals() - 2)) break elif obj2 < obj0 and obj2 < obj1 and obj2 < obj3: success = True alpha *= self.c2 if logger: - logger.addToLog( - "\tc2 best step-length value of: %.2e (feval = %d)" % (alpha, problem.get_fevals() - 1)) + logger.addToLog("\tc2 best step-length value of: %.2e (feval = %d)" % (alpha, problem.get_fevals() - 1)) break elif obj3 < obj0 and obj3 <= obj1 and obj3 <= obj2: success = True alpha *= step_scale if logger: - logger.addToLog("\tparabola minimum best step-length value of: %.2e (feval = %d)" % ( - alpha, problem.get_fevals())) + logger.addToLog("\tparabola minimum best step-length value of: %.2e (feval = %d)" % (alpha, problem.get_fevals())) break else: # Shrink line search alpha *= self.shrink if logger: logger.addToLog("\tShrinking search direction") - + if success: - # Line search has finished, update domain + # Line search has finished, update model self.alpha = deepcopy(alpha) model_step.copy(modl) # model_step = m_current model_step.scaleAdd(dmodl, sc2=self.alpha) - # Checking if domain parameters hit the bounds + # Checking if model parameters hit the bounds modl.copy(model_step) - # Projecting domain onto the bounds (if any) + # Projecting model onto the bounds (if any) if "bounds" in dir(problem): problem.bounds.apply(model_step) if modl.isDifferent(model_step): @@ -807,7 +773,7 @@ def run(self, problem, modl, dmodl, logger=None): dmodl.scaleAdd(modl, 1.0, -1.0) # Scaled by the inverse of the step length dmodl.scale(1.0 / self.alpha) - # Setting domain and residual vectors to c1 or c2 point if parabola minimum is not picked + # Setting model and residual vectors to c1 or c2 point if parabola minimum is not picked problem.set_model(model_step) if obj1 < obj0 and obj1 < obj2 and obj1 < obj3: problem.set_residual(res1) @@ -821,7 +787,7 @@ def run(self, problem, modl, dmodl, logger=None): class ParabolicStepConst(Stepper): """Parabolic Stepper class assuming constant local curvature""" - + def __init__(self, c1=1.0, ntry=10, alpha=0., alpha_scale_min=1.0e-10, alpha_scale_max=2000.00, shrink=0.25): """ Constructor for parabolic stepper assuming constant local curvature: @@ -841,21 +807,20 @@ def __init__(self, c1=1.0, ntry=10, alpha=0., alpha_scale_min=1.0e-10, alpha_sca self.zero = 10 ** (np.floor( np.log10(np.abs(float(np.finfo(np.float64).tiny)))) + 2) # Check for avoid Overflow or Underflow return - + def run(self, problem, modl, dmodl, logger=None): """Method to apply parabolic stepper""" # Writing to log file if any if logger: logger.addToLog("PARABOLIC STEPPER ASSUMING CONSTANT LOCAL CURVATURE") - logger.addToLog( - "c1=%.2e ntry=%d steplength-scaling-min=%.2e steplength-scaling-max=%.2e shrinking-factor=%.2e" - % (self.c1, self.ntry, self.alpha_scale_min, self.alpha_scale_max, self.shrink)) + logger.addToLog("c1=%.2e ntry=%d steplength-scaling-min=%.2e steplength-scaling-max=%.2e shrinking-factor=%.2e" + % (self.c1, self.ntry, self.alpha_scale_min, self.alpha_scale_max, self.shrink)) success = False - # Obtain objective function for provided domain + # Obtain objective function for provided model obj0 = problem.get_obj(modl) # Model temporary vector model_step = modl.clone() - # Getting pointer to problem's domain vector + # Getting pointer to problem's model vector prblm_mdl = problem.get_model() # Initial step length value alpha = deepcopy(self.alpha) @@ -887,9 +852,9 @@ def run(self, problem, modl, dmodl, logger=None): logger.addToLog("\tTesting point (c1=%.2e): m_current+c1*alpha*dm" % self.c1) model_step.copy(modl) model_step.scaleAdd(dmodl, sc2=self.c1 * alpha) - # Checking if domain parameters hit the bounds + # Checking if model parameters hit the bounds problem.set_model(model_step) - # Projecting domain onto the bounds (if any) and rotate search direction + # Projecting model onto the bounds (if any) and rotate search direction if "bounds" in dir(problem): problem.bounds.apply(model_step) if prblm_mdl.isDifferent(model_step): @@ -915,7 +880,7 @@ def run(self, problem, modl, dmodl, logger=None): if itry >= self.ntry: if logger: logger.addToLog("\t!!!Check problem definition or change solver!!!") - # Setting domain to current one and resetting initial step length value + # Setting model to current one and resetting initial step length value alpha = 0.0 self.alpha = 0.0 problem.set_model(modl) @@ -940,13 +905,11 @@ def run(self, problem, modl, dmodl, logger=None): alpha_parab = - phi_der / c step_scale = alpha_parab / alpha if logger: - logger.addToLog( - "\tTesting point (c_opt=%.2e): m_current+c_opt*alpha*dm (parabola minimum)" % step_scale) + logger.addToLog("\tTesting point (c_opt=%.2e): m_current+c_opt*alpha*dm (parabola minimum)" % step_scale) # If step length negative, re-evaluate points if alpha_parab < 0.: if logger: - logger.addToLog( - "\tEncountered a negative step-length value: %.2e; Shrinking step-length value." % alpha_parab) + logger.addToLog("\tEncountered a negative step-length value: %.2e; Shrinking step-length value." % alpha_parab) # Shrink line search alpha *= self.shrink itry += 1 @@ -955,23 +918,21 @@ def run(self, problem, modl, dmodl, logger=None): if step_scale < self.alpha_scale_min: if logger: logger.addToLog("\t!!! step-length scale of %.2e smaller than provided lower bound." - "Clipping its value to bound value of %.2e !!!" % ( - step_scale, self.alpha_scale_min)) + "Clipping its value to bound value of %.2e !!!" % (step_scale, self.alpha_scale_min)) step_scale = self.alpha_scale_min elif step_scale > self.alpha_scale_max: if logger: logger.addToLog("\t!!! step-length scale of %.2e greater than provided upper bound." - "Clipping its value to bound value of %.2e !!!" % ( - step_scale, self.alpha_scale_max)) + "Clipping its value to bound value of %.2e !!!" % (step_scale, self.alpha_scale_max)) step_scale = self.alpha_scale_max - + # Testing parabolic scale # Compute new objective function at the minimum of the parabolic approximation model_step.copy(modl) model_step.scaleAdd(dmodl, sc2=alpha * step_scale) - # Checking if domain parameters hit the bounds + # Checking if model parameters hit the bounds problem.set_model(model_step) - # Projecting domain onto the bounds (if any) + # Projecting model onto the bounds (if any) if "bounds" in dir(problem): problem.bounds.apply(model_step) if prblm_mdl.isDifferent(model_step): @@ -981,7 +942,7 @@ def run(self, problem, modl, dmodl, logger=None): obj2 = problem.get_obj(model_step) if logger: logger.addToLog("\tObjective function value of %.5e" % obj2) - + # Writing info to log file if logger: logger.addToLog("\tInitial objective function value: %2e," @@ -989,36 +950,34 @@ def run(self, problem, modl, dmodl, logger=None): "Objective function at parabola minimum: %.2e" % (obj0, obj1, obj2)) itry += 1 - + # Check which one is the best step length if obj1 < obj0 and obj1 < obj2: success = True alpha *= self.c1 if logger: - logger.addToLog( - "\tc1 best step-length value of: %.2e (feval = %d)" % (alpha, problem.get_fevals() - 1)) + logger.addToLog("\tc1 best step-length value of: %.2e (feval = %d)" % (alpha, problem.get_fevals() - 1)) break elif obj2 < obj0 and obj2 <= obj1: success = True alpha *= step_scale if logger: - logger.addToLog("\tparabola minimum best step-length value of: %.2e (feval = %d)" % ( - alpha, problem.get_fevals())) + logger.addToLog("\tparabola minimum best step-length value of: %.2e (feval = %d)" % (alpha, problem.get_fevals())) break else: # Shrink line search alpha *= self.shrink if logger: logger.addToLog("\tShrinking search direction") - + if success: - # Line search has finished, update domain + # Line search has finished, update model self.alpha = deepcopy(alpha) model_step.copy(modl) # model_step = m_current model_step.scaleAdd(dmodl, sc2=self.alpha) - # Checking if domain parameters hit the bounds + # Checking if model parameters hit the bounds modl.copy(model_step) - # Projecting domain onto the bounds (if any) + # Projecting model onto the bounds (if any) if "bounds" in dir(problem): problem.bounds.apply(model_step) if modl.isDifferent(model_step): @@ -1027,7 +986,7 @@ def run(self, problem, modl, dmodl, logger=None): dmodl.scaleAdd(modl, 1.0, -1.0) # Scaled by the inverse of the step length dmodl.scale(1.0 / self.alpha) - # Setting domain and residual vectors to c1 or c2 point if parabola minimum is not picked + # Setting model and residual vectors to c1 or c2 point if parabola minimum is not picked problem.set_model(model_step) if obj1 < obj0 and obj1 < obj2: problem.set_residual(res1) @@ -1042,8 +1001,8 @@ class StrongWolfe(Stepper): Algorithm 3.5. Page 60. "Numerical Optimization". Nocedal & Wright. Implementation based on the ones in the GitHub repo: https://github.com/bgranzow/L-BFGS-B.git """ - - def __init__(self, c1=1.e-4, c2=0.9, ntry=20, alpha=1., alpha_scale=0.8, alpha_max=2.5, keepAlpha=False): + + def __init__(self, c1=1.e-4, c2=0.9 , ntry=20, alpha=1., alpha_scale=0.8, alpha_max=2.5, keepAlpha=False): """ Constructor for parabolic stepper assuming constant local curvature: c1 = [1.e-4] - float; c1 value to tests first Wolfe condition (should be between 0 and 1) @@ -1064,14 +1023,14 @@ def __init__(self, c1=1.e-4, c2=0.9, ntry=20, alpha=1., alpha_scale=0.8, alpha_m np.log10(np.abs(float(np.finfo(np.float64).tiny)))) + 2) # Check for avoid Overflow or Underflow self.keepAlpha = keepAlpha return - + def alpha_zoom(self, problem, mdl0, mdl, obj0, dphi0, dmodl, alpha_lo, alpha_hi, logger=None): """Algorithm 3.6, Page 61. "Numerical Optimization". Nocedal & Wright.""" itry = 0 alpha = 0.0 while itry < self.ntry: if logger: - logger.addToLog("\t\ttrial number [alpha_zoom]: %d" % (itry + 1)) + logger.addToLog("\t\ttrial number [alpha_zoom]: %d" % (itry+1)) alpha_i = 0.5 * (alpha_lo + alpha_hi) alpha = alpha_i # x = x0 + alpha_i * p @@ -1080,12 +1039,10 @@ def alpha_zoom(self, problem, mdl0, mdl, obj0, dphi0, dmodl, alpha_lo, alpha_hi, # Evaluating objective and gradient function obj_i = problem.get_obj(mdl) if logger: - logger.addToLog( - "\t\tObjective function value of %.5e at m_i with alpha=%.5e [alpha_zoom]" % (obj_i, alpha_i)) + logger.addToLog("\t\tObjective function value of %.5e at m_i with alpha=%.5e [alpha_zoom]" %(obj_i, alpha_i)) if isnan(obj_i): if logger: - logger.addToLog( - "\t\t!!!Problem with step length and objective function; Setting alpha = 0.0 [alpha_zoom]!!!") + logger.addToLog("\t\t!!!Problem with step length and objective function; Setting alpha = 0.0 [alpha_zoom]!!!") alpha = 0.0 break grad_i = problem.get_grad(mdl) @@ -1094,12 +1051,10 @@ def alpha_zoom(self, problem, mdl0, mdl, obj0, dphi0, dmodl, alpha_lo, alpha_hi, mdl.scaleAdd(dmodl, sc2=alpha_lo) # x = x0 + alpha_i * p; obj_lo = problem.get_obj(mdl) if logger: - logger.addToLog( - "\t\tObjective function value of %.5e at m_lo with alpha_lo=%.5e [alpha_zoom]" % (obj_lo, alpha_lo)) + logger.addToLog("\t\tObjective function value of %.5e at m_lo with alpha_lo=%.5e [alpha_zoom]" %(obj_lo, alpha_lo)) if isnan(obj_lo): if logger: - logger.addToLog( - "\t\t!!!Problem with step length and objective function; Setting alpha = 0.0 [alpha_zoom]!!!") + logger.addToLog("\t\t!!!Problem with step length and objective function; Setting alpha = 0.0 [alpha_zoom]!!!") alpha = 0.0 break if (obj_i > obj0 + self.c1 * alpha_i * dphi0) or (obj_i >= obj_lo): @@ -1118,7 +1073,7 @@ def alpha_zoom(self, problem, mdl0, mdl, obj0, dphi0, dmodl, alpha_lo, alpha_hi, alpha = alpha_i break return alpha - + def run(self, problem, modl, dmodl, logger=None): """Method to apply line search that satisfies strong Wolfe conditions""" # Writing to log file if any @@ -1128,7 +1083,7 @@ def run(self, problem, modl, dmodl, logger=None): "c1=%.2e c2=%.2e ntry=%d alpha-max=%.2e keepAlpha=%s" % (self.c1, self.c2, self.ntry, self.alpha_max, self.keepAlpha)) success = False - # Obtain objective function for provided domain + # Obtain objective function for provided model obj0 = problem.get_obj(modl) obj_im1 = deepcopy(obj0) # Model temporary vector @@ -1144,7 +1099,7 @@ def run(self, problem, modl, dmodl, logger=None): while itry < self.ntry: # Writing info to log file if logger: - logger.addToLog("\ttrial number: %d" % (itry + 1)) + logger.addToLog("\ttrial number: %d" % (itry+1)) logger.addToLog("\tinitial-steplength=%.2e" % alpha_i) # Find the first guess as if the problem was linear (Tangent method) if alpha_i <= self.zero: @@ -1152,10 +1107,10 @@ def run(self, problem, modl, dmodl, logger=None): self.alpha_max *= alpha_i if logger: logger.addToLog("\tGuessing step length of: %.2e" % alpha_i) - - # Updating domain point - model_step.copy(modl) # x = x0 - model_step.scaleAdd(dmodl, sc2=alpha_i) # x = x0 + alpha_i * p; + + # Updating model point + model_step.copy(modl) # x = x0 + model_step.scaleAdd(dmodl, sc2=alpha_i) # x = x0 + alpha_i * p; # Evaluating objective and gradient function obj_i = problem.get_obj(model_step) if logger: @@ -1171,7 +1126,7 @@ def run(self, problem, modl, dmodl, logger=None): logger.addToLog("\tCondition 1 matched; step-length value of: %.2e" % alpha) success = True break - + # dphi = transpose(g_i) * p; dphi = grad_i.dot(dmodl) if np.abs(dphi) <= -self.c2 * dphi0: @@ -1186,28 +1141,27 @@ def run(self, problem, modl, dmodl, logger=None): logger.addToLog("\tCondition 3 matched; step-length value of: %.2e" % alpha) success = True break - + # Update step-length value alpha_im1 = alpha_i obj_im1 = obj_i alpha_i = alpha_i + self.alpha_scale * (self.alpha_max - alpha_i) - + if itry > self.ntry: alpha = alpha_i if logger: - logger.addToLog("\tMaximum number of trials reached (ntry = %d); step-length value of: %.2e" % ( - self.ntry, alpha)) + logger.addToLog("\tMaximum number of trials reached (ntry = %d); step-length value of: %.2e" %(self.ntry,alpha)) success = True break - + # Update trial number itry += 1 if success: - # Line search has finished, update domain + # Line search has finished, update model modl.scaleAdd(dmodl, sc2=alpha) if self.keepAlpha: self.alpha = alpha # Delete temporary vectors del model_step - - return alpha, success + + return alpha, success \ No newline at end of file diff --git a/occamypy/solver/stopper.py b/occamypy/solver/stopper.py index d30ad6a..c4482da 100644 --- a/occamypy/solver/stopper.py +++ b/occamypy/solver/stopper.py @@ -1,64 +1,47 @@ import time from timeit import default_timer as timer - import numpy as np -from occamypy.problem.base import Problem -from occamypy.utils.logger import Logger - - -def _get_zfill(integer: int) -> int: - """ - Args: - integer: query iteration number - - Returns: - number of digits for printing N - """ - digits = int(np.floor(np.log10(integer))) + 1 - return digits +from occamypy import problem as P class Stopper: """ Base stopper class. Used to implement stopping criteria for the solver. - + Methods: reset: reset the inner variables run(problem): apply its criteria to the problem """ - + def __init__(self): """Default class constructor for Stopper""" - self.zfill = 3 return - + def __del__(self): """Default destructor""" return - + def reset(self): """Function to reset stopper variables""" raise NotImplementedError("Implement reset stopper in the derived class.") - - def run(self, problem, verbose: bool = False, *args, **kwargs) -> bool: - """Apply stopping criteria to problem. Should return a boolean to stop the solver.""" + + def run(self, problem): + """Dummy stopper running method""" raise NotImplementedError("Implement run stopper in the derived class.") class BasicStopper(Stopper): """ Basic Stopper with different options - + Notes: stopper is going to change the gradient/obj/res files """ - def __init__(self, niter: int = 0, maxfevals: int = 0, maxhours: float = 0.0, tolr: float = 1e-32, - tolg: float = 1e-32, tolg_proj=None, - tolobj: float = None, tolobjrel: float = None, - toleta: float = None, tolobjchng: float = None, logger: Logger = None): + def __init__(self, niter=0, maxfevals=0, maxhours=0.0, tolr=1.0e-32, tolg=1.0e-32, tolg_proj=None, tolobj=None, tolobjrel=None, + toleta=None, tolobjchng=None, logger=None): """ BasicStopper constructor @@ -78,7 +61,7 @@ def __init__(self, niter: int = 0, maxfevals: int = 0, maxhours: float = 0.0, to # Criteria to evaluate whether or not to stop the solver super(BasicStopper, self).__init__() self.niter = niter - self.zfill = _get_zfill(self.niter) # number of digits for printing the iteration number + self.zfill = int(np.floor(np.log10(self.niter)) + 1) # number of digits for printing the iteration number self.maxfevals = maxfevals self.maxhours = maxhours self.tolr = tolr @@ -97,14 +80,17 @@ def __init__(self, niter: int = 0, maxfevals: int = 0, maxhours: float = 0.0, to # Starting timer self.__start = timer() return - + def reset(self): + """Function to reset stopper variables""" + # Restarting timer self.__start = timer() self.obj_pts = list() return - - def run(self, problem: Problem, niter: int, initial_obj_value: float = None, verbose: bool = False) -> bool: - if not isinstance(problem, Problem): + + # Beware stopper is going to change the gradient/obj/res files + def run(self, problem, niter, initial_obj_value=None, verbose=False): + if not isinstance(problem, P.Problem): raise TypeError("Input variable is not a Problem object") # Variable to impose stopping to solver stop = False @@ -188,7 +174,7 @@ def run(self, problem: Problem, niter: int, initial_obj_value: float = None, ver if self.tolobj is not None: if obj < self.tolobj: stop = True - msg = "Terminate: objective function value tolerance of %s reached, objective function value %s\n" \ + msg = "Terminate: objective function value tolerance of %s reached, objective function value %s\n"\ % (self.tolobj, obj) if verbose: print(msg) @@ -209,7 +195,7 @@ def run(self, problem: Problem, niter: int, initial_obj_value: float = None, ver data_norm = problem.data.norm() if res_norm < self.toleta * data_norm: stop = True - msg = "Terminate: eta tolerance (i.e., |Am - b|/|b|) of %s reached, eta value %s" \ + msg = "Terminate: eta tolerance (i.e., |Am - b|/|b|) of %s reached, eta value %s"\ % (self.toleta, res_norm / data_norm) if verbose: print(msg) @@ -239,18 +225,18 @@ def run(self, problem: Problem, niter: int, initial_obj_value: float = None, ver class SamplingStopper(Stopper): """ Stopper that check the number of tested samples. - + Examples: used in MCMC solver - + Notes: stopper is going to change the gradient/obj/res files """ - def __init__(self, nsamples: int, maxhours: float = 0., logger: Logger = None): + def __init__(self, nsamples, maxhours=0.0, logger=None): """ SamplingStopper constructor - + Args: nsamples: number of samples to test maxhours: maximum total running time in hours (must be greater than 0. to be checked) @@ -259,20 +245,22 @@ def __init__(self, nsamples: int, maxhours: float = 0., logger: Logger = None): # Criteria to evaluate whether or not to stop the solver super(SamplingStopper, self).__init__() self.nsamples = nsamples - self.zfill = _get_zfill(self.nsamples) + self.zfill = int(np.floor(np.log10(self.nsamples)) + 1) # number of digits for printing the iteration number self.maxhours = maxhours # Logger to write to file stopper information self.logger = logger # Starting timer self.__start = timer() return - + def reset(self): + """Function to reset stopper variables""" + # Restarting timer self.__start = timer() return - - def run(self, problem: Problem, nsamples: int, verbose: bool = False) -> bool: - if not isinstance(problem, Problem): + + def run(self, problem, nsamples, verbose=False): + if not isinstance(problem, P.Problem): raise TypeError("Input variable is not a Problem object") # Variable to impose stopping to solver stop = False diff --git a/occamypy/torch/autograd.py b/occamypy/torch/autograd.py index ce93c55..dd4faa6 100644 --- a/occamypy/torch/autograd.py +++ b/occamypy/torch/autograd.py @@ -19,7 +19,7 @@ class VectorAD(VectorTorch): def __init__(self, in_content, device: int = None, *args, **kwargs): """ VectorAD constructor - + Args: in_content: Vector, np.ndarray, torch.Tensor or tuple device: computation device (None for CPU, -1 for least used GPU) @@ -116,7 +116,7 @@ class AutogradFunction: def __init__(self, operator): """ AutogradFunction constructor - + Args: operator: linear operator, can be based on any vector backend """ @@ -156,11 +156,12 @@ def __call__(self, other): # torch notation: self(tensor, vector) -> tensor def apply(self, model: torch.Tensor) -> torch.Tensor: return self.function(model, self.vec_class, self.fwd, self.adj, - self.op_dev, self.torch_comp,) + self.op_dev, self.torch_comp, ) if __name__ == "__main__": import occamypy as o + S = torch.nn.Sigmoid() # use with VectorTorch (requires_grad=False) @@ -170,8 +171,8 @@ def apply(self, model: torch.Tensor) -> torch.Tensor: sig_y_ = S(T(x)) y_sig = T(S(x[:])) y = T * x - del y,y_,sig_y_,y_sig - + del y, y_, sig_y_, y_sig + # use with VectorAD (requires_grad=True) to wrap learnable tensors x = VectorAD(torch.ones(2)) T = AutogradFunction(o.Scaling(x, 2)) @@ -179,8 +180,8 @@ def apply(self, model: torch.Tensor) -> torch.Tensor: sig_y_ = S(T(x)) y_sig = T(S(x[:])) y = T * x - del y,y_,sig_y_,y_sig - + del y, y_, sig_y_, y_sig + # now try a numpy-based operator on a tensor with requires_grad=False x = VectorTorch(torch.zeros(21)) x[10] = 1 @@ -189,8 +190,8 @@ def apply(self, model: torch.Tensor) -> torch.Tensor: sig_y_ = S(T(x)) y_sig = T(S(x[:])) y = T * x - del y,y_,sig_y_,y_sig - + del y, y_, sig_y_, y_sig + # now try a numpy-based operator on a tensor with requires_grad=True x = VectorAD(x) T = AutogradFunction(o.GaussianFilter(o.VectorNumpy((x.size,)), 1)) diff --git a/occamypy/torch/back_utils.py b/occamypy/torch/back_utils.py index 8adf804..dae8b35 100644 --- a/occamypy/torch/back_utils.py +++ b/occamypy/torch/back_utils.py @@ -1,7 +1,8 @@ -import numpy as np import torch +import numpy as np from GPUtil import getFirstAvailable + __all__ = [ "set_backends", "set_seed_everywhere", @@ -17,7 +18,7 @@ def set_backends(): torch.backends.cudnn.deterministic = True -def set_seed_everywhere(seed: int = 0): +def set_seed_everywhere(seed=0): """Set random seed for numpy and torch""" np.random.seed(seed) torch.manual_seed(seed) @@ -27,13 +28,12 @@ def set_seed_everywhere(seed: int = 0): def get_device(devID: int = None) -> torch.device: """ Get the computation device - + Args: devID: device id to be used (None for CPU, -1 for max free memory) Returns: torch.device object """ - if devID is None: dev = "cpu" elif devID == -1: @@ -50,11 +50,10 @@ def get_device(devID: int = None) -> torch.device: def get_device_name(devID: int = None) -> str: """ Get the device name as a nice string - + Args: devID: device ID for torch """ - if devID is None or isinstance(torch.cuda.get_device_name(devID), torch.NoneType): return "CPU" else: diff --git a/occamypy/torch/operator/signal.py b/occamypy/torch/operator/signal.py index 8d90fc9..98fa059 100644 --- a/occamypy/torch/operator/signal.py +++ b/occamypy/torch/operator/signal.py @@ -1,10 +1,9 @@ -from typing import Union, Tuple +from typing import Union, List, Tuple from itertools import accumulate, product import numpy as np import torch -from occamypy.vector.base import superVector -from occamypy.operator.base import Operator, Dstack +from occamypy import superVector, Operator, Dstack from occamypy.torch.vector import VectorTorch from occamypy.torch.back_utils import set_backends @@ -13,6 +12,7 @@ def _gaussian_kernel1d(sigma: float, order: int = 0, truncate: float = 4.) -> torch.Tensor: """Computes a 1-D Gaussian convolution kernel""" + radius = int(truncate * sigma + 0.5) if order < 0: raise ValueError('order must be non-negative') @@ -25,6 +25,11 @@ def _gaussian_kernel1d(sigma: float, order: int = 0, truncate: float = 4.) -> to if order == 0: return phi_x else: + # f(x) = q(x) * phi(x) = q(x) * exp(p(x)) + # f'(x) = (q'(x) + q(x) * p'(x)) * phi(x) + # p'(x) = -1 / sigma ** 2 + # Implement q'(x) + q(x) * p'(x) as a matrix operator and apply to the + # coefficients of q(x) q = torch.zeros(order + 1) q[0] = 1 D = torch.diag(exponent_range[1:], 1) # D @ q(x) = q'(x) @@ -42,7 +47,7 @@ class ConvND(Operator): def __init__(self, model: VectorTorch, kernel: Union[VectorTorch, torch.Tensor]): """ ConvND (torch) constructor - + Args: model: domain vector kernel: kernel vector or tensor @@ -80,7 +85,9 @@ def __init__(self, model: VectorTorch, kernel: Union[VectorTorch, torch.Tensor] padding=self.pad_size).flatten(end_dim=2) super(ConvND, self).__init__(model, model) - self.name = "Convolve" + + def __str__(self): + return " ConvND " def forward(self, add, model, data): self.checkDomainRange(model, data) @@ -100,7 +107,7 @@ def adjoint(self, add, model, data): class GaussianFilter(ConvND): """Gaussian smoothing operator""" - def __init__(self, model: VectorTorch, sigma: Tuple[float]): + def __init__(self, model, sigma): """ GaussianFilter (torch) constructor @@ -108,43 +115,44 @@ def __init__(self, model: VectorTorch, sigma: Tuple[float]): model: domain vector sigma: standard deviation along the domain directions """ - if not isinstance(sigma, tuple): - raise TypeError("sigma has to be a tuple") - self.sigma = sigma + self.sigma = [sigma] if isinstance(sigma, (float, int)) else sigma + if not isinstance(self.sigma, (list, tuple)): + raise TypeError("sigma has to be either a list or a tuple") self.scaling = np.sqrt(np.prod(np.array(self.sigma) / np.pi)) kernels = [_gaussian_kernel1d(s) for s in self.sigma] self.kernel = [*accumulate(kernels, lambda a, b: torch.outer(a.flatten(), b))][-1] self.kernel.reshape([k.numel() for k in kernels]) super(GaussianFilter, self).__init__(model, self.kernel.to(model.device)) - self.name = "GausFilt" + + def __str__(self): + return "GausFilt" -def Padding(domain: Union[VectorTorch, superVector], pad: Union[Tuple[int], Tuple[Tuple[int]]], mode: str = "constant"): +def Padding(model, pad, mode="constant"): """ Padding operator - + Args: domain: domain vector pad: Number of samples to pad in each dimension. If a single scalar is provided, it is assigned to every dimension. mode: padding mode (see https://pytorch.org/docs/1.10/generated/torch.nn.functional.pad.html) - + Examples: To pad 2 values to each side of the first dim, and 3 values to each side of the second dim, use: pad=(2,2,3,3) """ - - if isinstance(domain, VectorTorch): - return _Padding(domain, pad, mode) - elif isinstance(domain, superVector): + if isinstance(model, VectorTorch): + return _Padding(model, pad, mode) + elif isinstance(model, superVector): # TODO add the possibility to have different padding for each sub-vector - return Dstack([_Padding(v, pad, mode) for v in domain.vecs]) + return Dstack([_Padding(v, pad, mode) for v in model.vecs]) else: raise ValueError("ERROR! Provided domain has to be either vector or superVector") -def ZeroPad(domain: VectorTorch, pad: Union[Tuple[int], Tuple[Tuple[int]]]): +def ZeroPad(model, pad): """ Zero-Padding operator @@ -156,13 +164,13 @@ def ZeroPad(domain: VectorTorch, pad: Union[Tuple[int], Tuple[Tuple[int]]]): domain: domain vector pad: number of samples to be added at each end of the dimension, for each dimension """ - - return Padding(domain=domain, pad=pad, mode="constant") + return Padding(model, pad, mode="constant") class _Padding(Operator): - def __init__(self, model: VectorTorch, pad: Union[Tuple[int], Tuple[Tuple[int]]], mode: str = "constant"): + def __init__(self, model: VectorTorch, pad: Union[int, Tuple[int], List[int]], mode: str = "constant"): + nd = model.ndim if isinstance(pad, (int, float)): @@ -182,10 +190,12 @@ def __init__(self, model: VectorTorch, pad: Union[Tuple[int], Tuple[Tuple[int]]] self.inner_idx = [list(torch.arange(start=self.pad[0:-1:2][i], end=self.range.shape[i]-pad[1::2][i])) for i in range(nd)] self.mode = mode - self.name = "Padding" + + def __str__(self): + return "Padding " def forward(self, add, model, data): - """padding""" + """Pad the domain""" self.checkDomainRange(model, data) if not add: data.zero() diff --git a/occamypy/torch/operator/transform.py b/occamypy/torch/operator/transform.py index 4278097..63f4047 100644 --- a/occamypy/torch/operator/transform.py +++ b/occamypy/torch/operator/transform.py @@ -1,12 +1,10 @@ -from itertools import product - import torch import torch.fft as fft +from occamypy import Operator +from itertools import product from numpy.fft import fftfreq - -from occamypy.operator.base import Operator -from occamypy.torch.back_utils import set_backends -from occamypy.torch.vector import VectorTorch +from ..back_utils import set_backends +from ..vector import VectorTorch set_backends() @@ -18,10 +16,10 @@ class FFT(Operator): """N-dimensional Fast Fourier Transform for complex input""" - def __init__(self, model, axes=None, nfft: tuple = None, sampling: tuple = None): + def __init__(self, model, axes=None, nfft=None, sampling=None): """ FFT (torch) constructor - + Args: model: domain vector axes: dimension along which FFT is computed (all by default) @@ -56,7 +54,9 @@ def __init__(self, model, axes=None, nfft: tuple = None, sampling: tuple = None) super(FFT, self).__init__(domain=VectorTorch(torch.zeros(model.shape).type(torch.double)), range=VectorTorch(torch.zeros(dims_fft).type(torch.complex128))) - self.name = "FFT" + + def __str__(self): + return 'torchFFT' def forward(self, add, model, data): self.checkDomainRange(model, data) @@ -71,7 +71,7 @@ def adjoint(self, add, model, data): model.zero() # compute IFFT x = fft.ifftn(data.getNdArray(), s=self.nfft, dim=self.axes, norm='ortho').type(model.getNdArray().dtype) - # handle nfft > domain.shape + # handle nfft > model.shape x = torch.Tensor([x[coord] for coord in product(*self.inner_idx)]).reshape(self.domain.shape).to(model.device) model[:] += x return diff --git a/occamypy/torch/vector.py b/occamypy/torch/vector.py index 89a4dd6..aff7132 100644 --- a/occamypy/torch/vector.py +++ b/occamypy/torch/vector.py @@ -1,20 +1,19 @@ from math import sqrt import torch +from .back_utils import get_device, get_device_name from numpy import ndarray -from occamypy.torch.back_utils import get_device, get_device_name -from occamypy.vector.base import Vector +from occamypy import Vector class VectorTorch(Vector): """ Vector class based on torch.Tensor - + Notes: - tensors are stored in C-contiguous memory + tensors are stored in C-contiguous memory """ - def __init__(self, in_content, device: int = None, *args, **kwargs): """ VectorTorch constructor @@ -48,7 +47,7 @@ def __init__(self, in_content, device: int = None, *args, **kwargs): self.ndim = self.arr.ndim # Number of axes (integer) self.size = self.arr.numel() # Total number of elements (integer) - def _check_same_device(self, other) -> bool: + def _check_same_device(self, other): if not isinstance(self, VectorTorch): raise TypeError("The self vector has to be a VectorTorch") if not isinstance(other, VectorTorch): @@ -66,7 +65,6 @@ def device(self): return None def setDevice(self, devID): - """Set the computation device (CPU, GPU) of the vector""" if isinstance(devID, int): self.arr = self.arr.to(get_device(devID)) elif isinstance(devID, torch.device): @@ -74,7 +72,7 @@ def setDevice(self, devID): else: ValueError("Device type not understood") - def deviceName(self) -> str: + def deviceName(self): return get_device_name(self.device.index) def getNdArray(self): @@ -189,7 +187,7 @@ def real(self): self.getNdArray()[:] = torch.real(self.getNdArray()) return self - def imag(self, ): + def imag(self): self.getNdArray()[:] = torch.imag(self.getNdArray()) return self @@ -249,7 +247,7 @@ def isDifferent(self, other): isDiff = not torch.equal(self.getNdArray(), other.getNdArray()) return isDiff - def clip(self, low, high): + def clipVector(self, low, high): if not isinstance(low, VectorTorch): raise TypeError("Provided input low vector not a %s!" % self.whoami) if not isinstance(high, VectorTorch): diff --git a/occamypy/vector/__init__.py b/occamypy/vector/__init__.py index 421e453..96daea4 100644 --- a/occamypy/vector/__init__.py +++ b/occamypy/vector/__init__.py @@ -1,6 +1,6 @@ -from .axis_info import AxInfo from .base import * from .out_core import * +from .axis_info import AxInfo __all__ = [ "Vector", diff --git a/occamypy/vector/axis_info.py b/occamypy/vector/axis_info.py index c5d139d..cabbca2 100644 --- a/occamypy/vector/axis_info.py +++ b/occamypy/vector/axis_info.py @@ -6,7 +6,7 @@ class AxInfo(NamedTuple): """ Store information about vectors' axis - + Attributes: N: number of samples along the axis o: axis origin value (i.e., value the first sample) @@ -14,7 +14,7 @@ class AxInfo(NamedTuple): l: label of the axis last: value of the last sample """ - n : int = 1 + n: int = 1 o: float = 0. d: float = 1. l: str = "undefined" @@ -22,9 +22,9 @@ class AxInfo(NamedTuple): def to_string(self, ax: int = 1): """ Create a description of the axis - + Args: - ax: axis number for printing (for SEP-lib compatibility) + ax: axis number for printing (for SEPlib compatibility) Returns: string @@ -40,4 +40,4 @@ def plot(self): @property def last(self): - return self.o + (self.n-1) * self.d + return self.o + (self.n - 1) * self.d diff --git a/occamypy/vector/base.py b/occamypy/vector/base.py index 57f5825..8c715ea 100644 --- a/occamypy/vector/base.py +++ b/occamypy/vector/base.py @@ -1,9 +1,7 @@ import os - import numpy as np - from occamypy.utils import sep -from occamypy.vector.axis_info import AxInfo +from .axis_info import AxInfo class Vector: @@ -16,7 +14,7 @@ class Vector: ndim: number of dimensions dtype: array data type (e.g., float, int) based on the array backend ax_info: list of AxInfo objects describing the array axes - + Methods: getNdArray: to access the array norm: compute the vector N-norm @@ -54,7 +52,7 @@ class Vector: def __init__(self, ax_info: list = None): """ Vector constructor - + Args: ax_info: list of AxInfo objects about the vector axes """ @@ -72,7 +70,7 @@ def __repr__(self): return self.getNdArray().__repr__() def __del__(self): - return + """Default destructor""" def __add__(self, other): # self + other if type(other) in [int, float]: @@ -82,7 +80,7 @@ def __add__(self, other): # self + other self.scaleAdd(other) return self else: - raise TypeError('Argument has to be either scalar or vector, got %r instead' % other) + raise TypeError("Argument has to be either scalar or vector, got %r instead" % other) def __sub__(self, other): # self - other self.__add__(-other) @@ -136,9 +134,7 @@ def __setitem__(self, key, value): self.getNdArray()[key] = value def init_ax_info(self): - """ - Initialize the AxInfo list for every axis - """ + """Initialize the AxInfo list for every axis""" self.ax_info = [AxInfo(n=s) for s in self.shape] @property @@ -149,14 +145,13 @@ def whoami(self): def dtype(self): return self.getNdArray().dtype - # Class vector operations def getNdArray(self): """Get the vector content""" raise NotImplementedError("getNdArray must be overwritten") def norm(self, N=2): """Compute vector N-norm - + Args: N: vector norm type """ @@ -177,7 +172,7 @@ def min(self): def set(self, val): """Fill the vector with a value - + Args: val: value to fill the vector """ @@ -193,8 +188,9 @@ def addbias(self, bias): self.getNdArray()[:] += bias return self - def rand(self, snr: float = 1.): + def rand(self): """Fill vector with random number ~U[1,-1] with a given SNR + Args: snr: SNR value """ @@ -202,6 +198,7 @@ def rand(self, snr: float = 1.): def randn(self, snr: float = 1.): """Fill vector with random number ~N[0, 1] with a given SNR + Args: snr: SNR value """ @@ -222,7 +219,7 @@ def checkSame(self, other): def writeVec(self, filename, mode='w'): """ Write vector to file - + Args: filename: path/to/file.ext mode: 'a' for append, 'w' for overwriting @@ -352,11 +349,13 @@ def real(self): def imag(self): """Return the in-place imaginary part of the vector""" raise NotImplementedError('imag method must be implemented') - + + # Combination of different vectors + def copy(self, other): """ Copy input vector - + Args: other: vector to be copied """ @@ -373,7 +372,7 @@ def copy(self, other): def scaleAdd(self, other, sc1=1.0, sc2=1.0): """ Scale two vectors and add them to the first one - + Args: other: vector to be added sc1: scaling factor of self @@ -391,7 +390,7 @@ def scaleAdd(self, other, sc1=1.0, sc2=1.0): def dot(self, other): """Compute dot product - + Args: other: second vector """ @@ -399,7 +398,7 @@ def dot(self, other): def multiply(self, other): """Element-wise multiplication - + Args: other: second vector """ @@ -407,16 +406,16 @@ def multiply(self, other): def isDifferent(self, other): """Check if two vectors are identical - + Args: other: second vector """ raise NotImplementedError("isDifferent must be overwritten") - def clip(self, low, high): + def clipVector(self, low, high): # TODO rename """ Bound vector values between two values - + Args: low: lower bound value high: upper bound value @@ -425,21 +424,20 @@ def clip(self, low, high): def plot(self): """Get a plottable array""" - return self.getNdArray() + raise NotImplementedError("plot must be overwritten") -# Set of vectors (useful to store results and same-Space vectors together) class VectorSet: """Class to store different vectors that live in the same Space""" def __init__(self): - """Class to store different vectors that live in the same Space""" - self.vecSet = [] # List of vectors of the set + """VectorSet constructor""" + self.vecSet = [] def __del__(self): """Default destructor""" - def append(self, other: Vector, copy: bool = True): + def append(self, other, copy=True): """Method to add vector to the set Args: other: vector to be added @@ -452,17 +450,14 @@ def append(self, other: Vector, copy: bool = True): self.vecSet.append(other.clone()) if copy else self.vecSet.append(other) - def writeSet(self, filename, mode: str = "a"): + def writeSet(self, filename, mode="a"): """ Write the set to file, all the vectors in set will be appended + Args: filename: path/to/file.ext mode: 'a' for append, 'w' for overwriting - - Returns: - """ - """Method to write to SEPlib file (by default it appends vectors to file)""" if mode not in "aw": raise ValueError("ERROR! mode must be either a (append) or w (write)") for idx_vec, vec_i in enumerate(self.vecSet): @@ -476,11 +471,11 @@ def writeSet(self, filename, mode: str = "a"): class superVector(Vector): """Class to handle a list of vectors as one""" - + def __init__(self, *args): """ superVector constructor - + Args: *args: vectors or superVectors or vectors list objects """ @@ -627,9 +622,9 @@ def isDifferent(self, vecs_in): raise TypeError("Input variable is not a superVector") return any([self.vecs[idx].isDifferent(vecs_in.vecs[idx]) for idx in range(self.n)]) - def clip(self, lows, highs): + def clipVector(self, lows, highs): for idx in range(self.n): - self.vecs[idx].clip(lows[idx], highs[idx]) + self.vecs[idx].clipVector(lows[idx], highs[idx]) return self def abs(self): diff --git a/occamypy/vector/out_core.py b/occamypy/vector/out_core.py index ae82c60..e414f03 100644 --- a/occamypy/vector/out_core.py +++ b/occamypy/vector/out_core.py @@ -1,15 +1,14 @@ +import numpy as np import os +from time import time from copy import deepcopy -from re import compile from shutil import copyfile -from time import time -import numpy as np - -from occamypy.utils import sep, RunShellCmd, hashfile -from occamypy.utils.os import BUF_SIZE -from occamypy.vector.base import Vector +from .base import Vector +from occamypy.utils import sep +from occamypy.utils.os import RunShellCmd, hashfile, BUF_SIZE +from re import compile re_dpr = compile("DOT RESULT(.*)") @@ -19,7 +18,7 @@ class VectorOC(Vector): def __init__(self, in_content): """ VectorOC constructor - + Args: in_content: numpy array, header file, Vector instance """ @@ -92,7 +91,7 @@ def scale(self, sc): get_stat=False, get_output=False) return - def rand(self, snr: float = 1.): + def rand(self, snr=1.0): # Computing RMS amplitude of the vector rms = RunShellCmd("Attr < %s want=rms param=1 maxsize=5000" % (self.vecfile), get_stat=False)[0] rms = float(rms.split("=")[1]) # Standard deviation of the signal @@ -173,7 +172,7 @@ def writeVec(self, filename, mode='w'): n_vec = axes[self.ndim][0] append_dim = self.ndim + 1 with open(filename, mode) as fid: - fid.write("\n%s=%s o%s=0.0 d%s=1.0 \n" % (append_dim, n_vec + 1, append_dim, append_dim)) + fid.write("n%s=%s o%s=0.0 d%s=1.0 \n" % (append_dim, n_vec + 1, append_dim, append_dim)) fid.close() # Writing or Copying binary file if not (os.path.isfile(binfile) and mode in 'a'): diff --git a/tutorials/devitoseismic/preset_models.py b/tutorials/devitoseismic/preset_models.py index a3f37e6..e34b8f9 100644 --- a/tutorials/devitoseismic/preset_models.py +++ b/tutorials/devitoseismic/preset_models.py @@ -51,7 +51,7 @@ def demo_model(preset, **kwargs): fs = kwargs.pop('fs', False) if preset.lower() in ['constant-elastic']: - # A constant single-layer model in a 2D or 3D domain + # op constant single-layer model in a 2D or 3D domain # with velocity 1.5 km/s. vs = 0.5 * vp b = 1.0 @@ -61,7 +61,7 @@ def demo_model(preset, **kwargs): nbl=nbl, **kwargs) if preset.lower() in ['constant-viscoelastic']: - # A constant single-layer model in a 2D or 3D domain + # op constant single-layer model in a 2D or 3D domain # with velocity 2.2 km/s. qp = kwargs.pop('qp', 100.) vs = kwargs.pop('vs', 1.2) @@ -74,14 +74,14 @@ def demo_model(preset, **kwargs): **kwargs) if preset.lower() in ['constant-isotropic']: - # A constant single-layer model in a 2D or 3D domain + # op constant single-layer model in a 2D or 3D domain # with velocity 1.5 km/s. return SeismicModel(space_order=space_order, vp=vp, origin=origin, shape=shape, dtype=dtype, spacing=spacing, nbl=nbl, fs=fs, **kwargs) if preset.lower() in ['constant-viscoacoustic']: - # A constant single-layer model in a 2D or 3D domain + # op constant single-layer model in a 2D or 3D domain # with velocity 1.5 km/s. qp = kwargs.pop('qp', 100.) b = 1/2. @@ -91,7 +91,7 @@ def demo_model(preset, **kwargs): spacing=spacing, **kwargs) elif preset.lower() in ['constant-tti']: - # A constant single-layer model in a 2D or 3D domain + # op constant single-layer model in a 2D or 3D domain # with velocity 1.5 km/s. v = np.empty(shape, dtype=dtype) v[:] = 1.5 @@ -107,7 +107,7 @@ def demo_model(preset, **kwargs): delta=delta, theta=theta, phi=phi, bcs="damp", **kwargs) elif preset.lower() in ['layers-isotropic']: - # A n-layers model in a 2D or 3D domain with two different + # op n-layers model in a 2D or 3D domain with two different # velocities split across the height dimension: # By default, the top part of the domain has 1.5 km/s, # and the bottom part of the domain has 2.5 km/s. @@ -126,7 +126,7 @@ def demo_model(preset, **kwargs): fs=fs, **kwargs) elif preset.lower() in ['layers-elastic']: - # A n-layers model in a 2D or 3D domain with two different + # op n-layers model in a 2D or 3D domain with two different # velocities split across the height dimension: # By default, the top part of the domain has 1.5 km/s, # and the bottom part of the domain has 2.5 km/s. @@ -151,7 +151,7 @@ def demo_model(preset, **kwargs): elif preset.lower() in ['layers-viscoelastic', 'twolayer-viscoelastic', '2layer-viscoelastic']: - # A two-layer model in a 2D or 3D domain with two different + # op two-layer model in a 2D or 3D domain with two different # velocities split across the height dimension: # By default, the top part of the domain has 1.6 km/s, # and the bottom part of the domain has 2.2 km/s. @@ -195,7 +195,7 @@ def demo_model(preset, **kwargs): nbl=nbl, **kwargs) elif preset.lower() in ['layers-tti', 'layers-tti-noazimuth']: - # A n-layers model in a 2D or 3D domain with two different + # op n-layers model in a 2D or 3D domain with two different # velocities split across the height dimension: # By default, the top part of the domain has 1.5 km/s, # and the bottom part of the domain has 2.5 km/s.\ @@ -229,7 +229,7 @@ def demo_model(preset, **kwargs): return model elif preset.lower() in ['circle-isotropic']: - # A simple circle in a 2D domain with a background velocity. + # op simple circle in a 2D domain with a background velocity. # By default, the circle velocity is 2.5 km/s, # and the background veloity is 3.0 km/s. vp = kwargs.pop('vp_circle', 3.0) @@ -326,7 +326,7 @@ def demo_model(preset, **kwargs): delta=delta, theta=theta, phi=phi, bcs="damp", **kwargs) elif preset.lower() in ['layers-viscoacoustic']: - # A n-layers model in a 2D or 3D domain with two different + # op n-layers model in a 2D or 3D domain with two different # velocities split across the height dimension: # By default, the top part of the domain has 1.5 km/s, # and the bottom part of the domain has 3.5 km/s. diff --git a/tutorials/devitoseismic/source.py b/tutorials/devitoseismic/source.py index 785d3f8..c02cc75 100644 --- a/tutorials/devitoseismic/source.py +++ b/tutorials/devitoseismic/source.py @@ -266,7 +266,7 @@ class RickerSource(WaveletSource): Returns ---------- - A Ricker wavelet. + op Ricker wavelet. """ @property @@ -298,7 +298,7 @@ class GaborSource(WaveletSource): Returns ------- - A Gabor wavelet. + op Gabor wavelet. """ @property