"""Linear array of values which are used as variables of numerical functions
and parameter dependent matrices
Properties
----------
start : float
Starting value of the axis
step : float
Step between consecutive values of the axis
length : int
Length of the `data` array
data : numpy.array of float
Values of the axis
Examples
--------
Creation of the `ValueAxis` object
>>> va = ValueAxis(0.0,100,1.0)
Its standard attributes are the following:
>>> print(va.length)
100
>>> print(va.step)
1.0
>>> print(va.start)
0.0
The attribute `data` is an array of floats
>>> print(va.data[3:5])
[ 3. 4.]
Attributes `min` and `max` are provided for convenience
>>> print(va.min)
0.0
>>> print(va.max)
99.0
You can locate an index of a specific value. The lower neighbor and
the difference to it is returned.
>>> i,diff = va.locate(16.3)
>>> print(i)
16
>>> print("{0:.1f}".format(diff))
0.3
The following returns the nearest index on the axis
>>> i = va.nearest(16.3)
>>> print(i)
16
>>> i = va.nearest(16.7)
>>> print(i)
17
>>> i = va.nearest(16.5)
>>> print(i)
17
Mutual compatibility of the two ValueAxis objects can be tested as follows
>>> va1 = ValueAxis(0.0, 2000, 1.0)
>>> va2 = ValueAxis(0.0, 1000, 1.0)
>>> va2.is_subsection_of(va1)
True
>>> va1.is_subsection_of(va2)
False
>>> va1.is_extension_of(va2)
True
>>> va2.is_subsection_of(va1)
True
>>> va2.is_extension_of(va1)
False
>>> va1.is_equal_to(va2)
False
>>> va1.is_equal_to(va1)
True
>>> va3 = ValueAxis(0.0, 2000, 1.0)
>>> va3.is_equal_to(va1)
True
Class Details
-------------
"""
from __future__ import annotations
import numpy
from .. import REAL
from ..exceptions import QuantarheiError
from ..utils import Float, Integer
from .saveable import Saveable
[docs]
class ValueAxis(Saveable):
"""Linear array of values used as the argument axis of numerical functions.
Parameters
----------
start : float
First value of the axis.
length : int
Number of points on the axis.
step : float
Spacing between consecutive axis values.
Attributes
----------
data : numpy.ndarray
Array of axis values, length ``length``.
min : float
Minimum (first) value on the axis.
max : float
Maximum (last) value on the axis.
"""
step = Float("step")
start = Float("start")
length = Integer("length")
def __init__(self, start: float = 0.0, length: int = 1, step: float = 1.0) -> None:
self.step = step
self.start = start
self.length = length
self.data: numpy.ndarray = numpy.linspace(
start, start + (length - 1) * step, length, dtype=REAL
)
@property
def min(self) -> float:
"""Returns the minimum value on the axis"""
return self.start
@property
def max(self) -> float:
"""Returns the maximum value on the axis"""
return self.data[self.length - 1]
[docs]
def locate(self, val: float) -> tuple[int, float]:
"""Return the index of the lower neighbor of ``val`` and the distance.
Parameters
----------
val : float
A value within the ``[min, max]`` range of the axis.
Returns
-------
int
Index of the largest axis point that is ``<= val``.
float
Distance from that axis point to ``val``.
Raises
------
Exception
If ``val`` is outside the axis bounds.
"""
# nearest smaller neighbor index
nsni = int(numpy.floor((val - self.start) / self.step))
# if n0 is within bounds calculate distance
# from the lower neighbor
if (nsni >= 0) and (nsni < self.length):
dval = val - self.data[nsni]
return nsni, dval
raise QuantarheiError("Value out of bounds")
[docs]
def nearest(self, val: float) -> int:
"""Return the index of the nearest axis point to ``val``.
If ``val`` is within one step above the upper bound, the index of
the last point is returned.
Parameters
----------
val : float
A value within (or just above) the ``[min, max]`` range of the
axis.
Returns
-------
int
Index of the axis point closest to ``val``.
Raises
------
Exception
If ``val`` is outside the axis bounds.
"""
# nearest smaller neighbor index
nsni = int(numpy.floor((val - self.start) / self.step))
if (nsni >= 0) and (nsni < self.length):
# if n0 is with bounds calculate difference
# from the lower neighbor
diff1 = numpy.abs(val - self.data[nsni])
# if the upper neighbor is within bounds calculate difference
# from the upper neighbor
if nsni + 1 < self.length:
diff2 = numpy.abs(val - self.data[nsni + 1])
else:
diff2 = 5 * self.step
# return the closer neighbor
if diff1 < diff2:
return nsni
if nsni + 1 < self.length:
return nsni + 1
# if the upper neigbor is out of bounds
# return the lower one
return nsni
raise QuantarheiError("Value out of bounds")
[docs]
def is_equal_to(self, axis: ValueAxis) -> bool:
"""Returns True if the axis is equal to this ValueAxis"""
return (
numpy.isclose(self.start, axis.start)
and numpy.isclose(self.step, axis.step)
and (self.length == axis.length)
)
def __eq__(self, other: object) -> bool:
return self.is_equal_to(other) # type: ignore[arg-type]
[docs]
def is_extension_of(self, axis: ValueAxis) -> bool:
"""Returns True if the axis is contained in this ValueAxis"""
ret = True
ret = ret and (
self.start <= axis.start or numpy.isclose(self.start, axis.start)
)
ret = ret and numpy.isclose(self.step, axis.step)
ret = ret and (self.max >= axis.max or numpy.isclose(self.max, axis.max))
offsets = (axis.data - self.start) / self.step
ret = ret and numpy.any(numpy.isclose(offsets, numpy.rint(offsets)))
return ret
[docs]
def is_subsection_of(self, axis: ValueAxis) -> bool:
"""Returns True if the axis contains this ValueAxis"""
ret = True
ret = ret and numpy.isclose(self.step, axis.step)
ret = ret and _axis_value_is_in_axis(self.start, axis)
ret = ret and _axis_value_is_in_axis(self.max, axis)
return ret
[docs]
def is_subset_of(self, axis: ValueAxis) -> bool:
"""Returns True if the ValueAxis is a subset of axis
We check if all values of this axis are also values of the submitted
axis object.
Examples
--------
>>> ta1 = ValueAxis(0.0, 1000, 1.0)
>>> ta2 = ValueAxis(0.0, 490, 2.0)
>>> ta2.is_subset_of(ta1)
True
>>> ta1 = ValueAxis(0.0, 1000, 1.0)
>>> ta2 = ValueAxis(3.0, 900, 1.0)
>>> ta2.is_subset_of(ta1)
True
>>> ta1 = ValueAxis(0.0, 1000, 1.0)
>>> ta2 = ValueAxis(3.0, 400, 2.0)
>>> ta2.is_subset_of(ta1)
True
>>> ta1 = ValueAxis(0.0, 1000, 1.12345)
>>> ta2 = ValueAxis(3*1.12345, 400, 2*1.12345)
>>> ta2.is_subset_of(ta1)
True
>>> ta1 = ValueAxis(0.0, 1000, 1.12345)
>>> ta2 = ValueAxis(3.0, 400, 2*1.12345)
>>> ta2.is_subset_of(ta1)
False
>>> ta1 = ValueAxis(0.0, 1000, 1.0)
>>> ta2 = ValueAxis(3.0, 500, 2.0)
>>> ta2.is_subset_of(ta1)
False
"""
ret = True
Nst = round(self.step / axis.step)
ret = ret and numpy.isclose(Nst * axis.step, self.step)
ret = ret and _axis_value_is_in_axis(self.start, axis)
ret = ret and (self.start < axis.max or numpy.isclose(self.start, axis.max))
ret = ret and _axis_value_is_in_axis(self.max, axis)
return ret
[docs]
def is_superset_of(self, axis: ValueAxis) -> bool:
"""Returns True if the ValueAxis is a superset of axis
We check if all values of this axis are also values of the submitted
axis object.
Examples
--------
>>> ta1 = ValueAxis(0.0, 1000, 1.0)
>>> ta2 = ValueAxis(0.0, 490, 2.0)
>>> ta1.is_superset_of(ta2)
True
>>> ta1 = ValueAxis(0.0, 1000, 1.0)
>>> ta2 = ValueAxis(3.0, 900, 1.0)
>>> ta1.is_superset_of(ta2)
True
>>> ta1 = ValueAxis(0.0, 1000, 1.0)
>>> ta2 = ValueAxis(3.0, 400, 2.0)
>>> ta1.is_superset_of(ta2)
True
>>> ta1 = ValueAxis(0.0, 1000, 1.12345)
>>> ta2 = ValueAxis(3*1.12345, 400, 2*1.12345)
>>> ta1.is_superset_of(ta2)
True
>>> ta1 = ValueAxis(0.0, 1000, 1.12345)
>>> ta2 = ValueAxis(3.0, 400, 2*1.12345)
>>> ta1.is_superset_of(ta2)
False
>>> ta1 = ValueAxis(0.0, 1000, 1.0)
>>> ta2 = ValueAxis(3.0, 500, 2.0)
>>> ta1.is_superset_of(ta2)
False
"""
ret = True
Nst = round(axis.step / self.step)
ret = ret and numpy.isclose(Nst * self.step, axis.step)
ret = ret and _axis_value_is_in_axis(axis.start, self)
ret = ret and (axis.start < self.max or numpy.isclose(axis.start, self.max))
ret = ret and _axis_value_is_in_axis(axis.max, self)
return ret
def __str__(self) -> str:
"""String representation of the ValueAxis object"""
out = "\nquantarhei.ValueAxis object"
out += "\n=========================="
out += "\nstart = " + str(self.start)
out += "\nlength = " + str(self.length)
out += "\nstep = " + str(self.step)
return out
def _axis_value_is_in_axis(value: float, axis: ValueAxis) -> bool:
"""Returns True when ``value`` lies on an axis grid point."""
if value < axis.min and not numpy.isclose(value, axis.min):
return False
if value > axis.max and not numpy.isclose(value, axis.max):
return False
nstep = round((value - axis.start) / axis.step)
return numpy.isclose(axis.start + nstep * axis.step, value)