Source code for schrodinger.math.mathutils
"""
Contains math-related utility functions
Copyright Schrodinger, LLC. All rights reserved.
"""
import math
import numpy
from collections import defaultdict
from scipy import interpolate
[docs]def round_value(val, precision=3, significant_figures=None):
"""
Return val as a string with the required precision or significant figures.
Either precision or significant_figures should be provided.
Uses scientific notation for very large or small values, if the precision allows.
:param float val: The value to round
:param int precision: The precision needed after rounding. A precision of 2 means
two decimal places should be kept after rounding. A negative precision indicates
how many digits can be replaced with zeros before the decimal point.
-5 means that 123456 can be shown as 1e5, -3 means it can be shown as 1.23e5.
:param int significant_figures: The number of significant figures that should
remain after rounding. If provided, determines the rounding precision.
:rtype: str or None
:return: A string with the required precision, or None if the input is a string
"""
if isinstance(val, str):
return None
val = float(val)
if val == 0:
return '0'
magnitude = int(numpy.floor(numpy.log10(abs(val))))
if significant_figures:
precision = significant_figures - magnitude - 1
# Uses scientific notation if more than 4 digits can be removed in large numbers
if magnitude < -3 or (magnitude > 3 and precision < -3):
decimals = max(0, magnitude + precision)
return f'{val:.{decimals}e}'
elif precision > 0:
return f'{val:.{precision}f}'
else:
return str(round(val))
[docs]def deduplicate_xy_data(x_vals, y_vals):
"""
Remove duplicate x values by averaging the y values for them.
:param list x_vals: The x values
:param list y_vals: The y values
:rtype: list, list
:return: The deduplicated xy data
"""
all_vals = defaultdict(list)
for x_val, y_val in zip(x_vals, y_vals):
all_vals[x_val].append(y_val)
deduped_x = list(all_vals.keys())
deduped_y = [sum(all_vals[x]) / len(all_vals[x]) for x in deduped_x]
return deduped_x, deduped_y
[docs]class Interpolate1D:
"""
Creates a map between values in a source and a target axis, allowing to get
the equivalent target point for each source point.
Wrapper around `scipy.interpolate.interp1d` to allow logarithmic
interpolation or extrapolation.
"""
[docs] def __init__(self, source_vals, target_vals, log_interp=False):
"""
Create an instance.
:param tuple source_vals: The values of points in the source range
:param tuple target_vals: The values of points in the target range
:param bool log_interp: Whether the interpolation is logarithmic.
If False, linear interpolation will be used.
"""
if log_interp:
target_vals = [numpy.log10(x) for x in target_vals]
linear_interp = interpolate.interp1d(
source_vals, target_vals, fill_value='extrapolate')
self.interp = lambda x: numpy.power(10, linear_interp(x))
else:
self.interp = interpolate.interp1d(
source_vals, target_vals, fill_value='extrapolate')
def __call__(self, source_val):
"""
Get the equivalent target value of the passed source value
:param float source_val: The value in the source range
:rtype: float
:return: The equivalent value in the target range
"""
return self.interp(source_val)
[docs]class Interpolate2D:
"""
Creates two instances of Interpolate1D to map values between two source axes
and two target axes. Example use case is mapping QGraphicsScene/QChart
XY coordinates to a XY coordinate system being displayed in the scene/chart.
The two axes need to be independent.
"""
[docs] def __init__(self,
x_source_vals,
x_target_vals,
y_source_vals,
y_target_vals,
x_log_interp=False,
y_log_interp=False):
"""
Create an instance.
:param tuple x_source_vals: The values of points in the X source range
:param tuple x_target_vals: The values of points in the X target range
:param tuple y_source_vals: The values of points in the Y source range
:param tuple y_target_vals: The values of points in the Y target range
:param bool x_log_interp: Whether the X axis interpolation is logarithmic
:param bool y_log_interp: Whether the Y axis interpolation is logarithmic
"""
self.x_interp = Interpolate1D(x_source_vals, x_target_vals,
x_log_interp)
self.y_interp = Interpolate1D(y_source_vals, y_target_vals,
y_log_interp)
def __call__(self, source_x_val, source_y_val):
"""
Get the equivalent target values of the passed source values
:param float source_x_val: The X value in the source range
:param float source_y_val: The Y value in the source range
:rtype: float, float
:return: The equivalent x, y values in the target range
"""
return (self.x_interp(source_x_val), self.y_interp(source_y_val))