Skip to content

Commit

Permalink
Add functionality to round to significant digits.
Browse files Browse the repository at this point in the history
  • Loading branch information
ronald-jaepel authored and schmoelder committed Jan 22, 2025
1 parent 0357f05 commit b415222
Show file tree
Hide file tree
Showing 8 changed files with 214 additions and 38 deletions.
1 change: 1 addition & 0 deletions CADETProcess/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
settings = Settings()

from . import sysinfo
from . import numerics
from . import dataStructure
from . import transform
from . import plotting
Expand Down
10 changes: 3 additions & 7 deletions CADETProcess/comparison/comparator.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from CADETProcess.dataStructure import get_nested_value
from CADETProcess.solution import SolutionBase
from CADETProcess.comparison import DifferenceBase
from CADETProcess.numerics import round_to_significant_digits


class Comparator(Structure):
Expand Down Expand Up @@ -338,12 +339,7 @@ def plot_comparison(
ax.legend(loc=1)

m = metric.evaluate(solution_sliced, slice=False)
m = [
np.format_float_scientific(
n, precision=2,
)
for n in m
]
m = round_to_significant_digits(m, digits=2)

text = f"{metric}: "
if metric.n_metrics > 1:
Expand All @@ -356,7 +352,7 @@ def plot_comparison(
except AttributeError:
text += f"{m}"
else:
text += m[0]
text += str(m[0])

plotting.add_text(ax, text, fontsize=14)

Expand Down
68 changes: 68 additions & 0 deletions CADETProcess/numerics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import numpy as np


def round_to_significant_digits(values: np.ndarray | list[float], digits: int) -> np.ndarray:
"""
Round an array of numbers to the specified number of significant digits.
Parameters
----------
values : np.ndarray | list[float]
Input array of floats to be rounded. Can be a NumPy array or a list of floats.
digits : int
Number of significant digits to retain. Must be greater than 0.
Returns
-------
np.ndarray
Array of values rounded to the specified significant digits.
The shape matches the input.
Notes
-----
- The function handles zero values by returning 0 directly, avoiding issues
with logarithms.
- Input arrays are automatically converted to `ndarray` if not already.
Examples
--------
>>> import numpy as np
>>> values = np.array([123.456, 0.001234, 98765, 0, -45.6789])
>>> round_to_significant_digits(values, 3)
array([ 123. , 0.00123, 98700. , 0. , -45.7 ])
>>> values = [1.2345e-5, 6.789e3, 0.0]
>>> round_to_significant_digits(values, 2)
array([ 1.2e-05, 6.8e+03, 0.0e+00])
"""

if digits is None:
return values

input_type = type(values)

values = np.asarray(values) # Ensure input is a NumPy array

if digits <= 0:
raise ValueError("Number of significant digits must be greater than 0.")

# Mask for non-zero values
nonzero_mask = values != 0
result = np.zeros_like(values) # Initialize result array

# For non-zero elements, calculate the scaling and apply rounding
if np.any(nonzero_mask): # Check if there are any non-zero values
nonzero_values = values[nonzero_mask]
scales = digits - np.floor(np.log10(np.abs(nonzero_values))).astype(int) - 1

# Round each non-zero value individually
rounded_nonzero = [
round(v, int(scale)) for v, scale in zip(nonzero_values, scales)
]

result[nonzero_mask] = rounded_nonzero # Assign the rounded values back

if input_type is not np.ndarray:
result = input_type(result)

return result
38 changes: 21 additions & 17 deletions CADETProcess/optimization/optimizationProblem.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
)

from CADETProcess.metric import MetricBase
from CADETProcess.numerics import round_to_significant_digits
from CADETProcess.optimization import Individual, Population
from CADETProcess.optimization import ResultsCache

Expand Down Expand Up @@ -331,7 +332,7 @@ def variable_values(self):
def add_variable(
self, name, evaluation_objects=-1, parameter_path=None,
lb=-math.inf, ub=math.inf, transform=None, indices=None,
precision=None, pre_processing=None):
significant_digits=None, pre_processing=None):
"""Add optimization variable to the OptimizationProblem.
The function encapsulates the creation of OptimizationVariable objects
Expand All @@ -358,7 +359,7 @@ def add_variable(
indices : int or tuple, optional
Indices for variables that modify entries of a parameter array.
If None, variable is assumed to be index independent.
precision : int, optional
significant_digits : int, optional
Number of significant figures to which variable can be rounded.
If None, variable is not rounded. The default is None.
pre_processing : callable, optional
Expand Down Expand Up @@ -404,7 +405,7 @@ def add_variable(
name, evaluation_objects, parameter_path,
lb=lb, ub=ub, transform=transform,
indices=indices,
precision=precision,
significant_digits=significant_digits,
pre_processing=pre_processing,
)

Expand Down Expand Up @@ -608,9 +609,6 @@ def get_dependent_values(self, X_independent: npt.ArrayLike) -> np.ndarray:

for i, x in enumerate(X_independent):
for indep_variable, indep_value in zip(independent_variables, x):
indep_value = np.format_float_positional(
indep_value, precision=indep_variable.precision, fractional=False
)
indep_variable.value = float(indep_value)

variable_values[i, :] = self.variable_values
Expand Down Expand Up @@ -2740,7 +2738,7 @@ def untransform(self, X_transformed: npt.ArrayLike) -> np.ndarray:

for i, ind in enumerate(X_transformed):
untransform[i, :] = [
var.untransform_fun(value, var.precision)
var.untransform_fun(value, significant_digits=var.significant_digits)
for value, var in zip(ind, self.independent_variables)
]

Expand Down Expand Up @@ -3020,9 +3018,9 @@ def create_initial_values(
ind = []
for i_var, var in enumerate(self.independent_variables):
ind.append(
float(np.format_float_positional(
float(round_to_significant_digits(
independent_values[i, i_var],
precision=var.precision, fractional=False
digits=var.significant_digits,
))
)

Expand Down Expand Up @@ -3392,7 +3390,7 @@ class OptimizationVariable:
indices : int, or slice
Indices for variables that modify an entry of a parameter array.
If None, variable is assumed to be index independent.
precision : int, optional
significant_digits : int, optional
Number of significant figures to which variable can be rounded.
If None, variable is not rounded. The default is None.
pre_processing : callable, optional
Expand All @@ -3409,7 +3407,7 @@ class OptimizationVariable:

def __init__(
self, name, evaluation_objects=None, parameter_path=None,
lb=-math.inf, ub=math.inf, transform=None, indices=None, precision=None,
lb=-math.inf, ub=math.inf, transform=None, indices=None, significant_digits=None,
pre_processing=None
):
self.name = name
Expand Down Expand Up @@ -3446,7 +3444,7 @@ def __init__(
raise ValueError("Unknown transform")

self._transform = transform
self.precision = precision
self.significant_digits = significant_digits

self._dependencies = []
self._dependency_transform = None
Expand Down Expand Up @@ -3729,7 +3727,9 @@ def value(self):
return self._value
else:
dependencies = [dep.value for dep in self.dependencies]
return self.dependency_transform(*dependencies)
value = self.dependency_transform(*dependencies)
value = round_to_significant_digits(value, self.significant_digits)
return value

@value.setter
def value(self, value):
Expand All @@ -3738,18 +3738,22 @@ def value(self, value):

self.set_value(value)

self._value = value

def set_value(self, value):
"""Set value to evaluation_objects."""
if not np.isscalar(value):
raise TypeError("Expected scalar value")

value = round_to_significant_digits(
value, digits=self.significant_digits,
)

if value < self.lb:
raise ValueError("Exceeds lower bound")
if value > self.ub:
raise ValueError("Exceeds upper bound")

self._value = value

if self.evaluation_objects is None:
return

Expand Down Expand Up @@ -3808,9 +3812,9 @@ def set_value(self, value):
new_value = parameter_type(new_value.tolist())

# Set the value:
self._set_value(eval_obj, new_value)
self._set_value_in_evaluation_object(eval_obj, new_value)

def _set_value(self, evaluation_object, value):
def _set_value_in_evaluation_object(self, evaluation_object, value):
"""Set the value to the evaluation object."""
if self.pre_processing is not None:
value = self.pre_processing(value)
Expand Down
9 changes: 5 additions & 4 deletions CADETProcess/transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import matplotlib.pyplot as plt

from CADETProcess import plotting
from CADETProcess.numerics import round_to_significant_digits


class TransformBase(ABC):
Expand Down Expand Up @@ -186,7 +187,7 @@ def _transform(self, x):
"""
pass

def untransform(self, x, precision=None):
def untransform(self, x, significant_digits=None):
"""Transform the output parameter space to the input parameter space.
Applies the transformation function _untransform to x after performing output
Expand All @@ -197,7 +198,7 @@ def untransform(self, x, precision=None):
----------
x : {float, array}
Output parameter values.
precision : int, optional
significant_digits : int, optional
Number of significant figures to which variable can be rounded.
If None, variable is not rounded. The default is None.
Expand All @@ -206,15 +207,15 @@ def untransform(self, x, precision=None):
{float, array}
Transformed parameter values.
"""
x_ = float(np.format_float_positional(x, precision=precision, fractional=False))
x_ = round_to_significant_digits(x, digits=significant_digits)

if (
not self.allow_extended_output and
not np.all((self.lb <= x_) * (x_ <= self.ub))):
raise ValueError("Value exceeds output bounds.")

x_ = self._untransform(x_)
x_ = float(np.format_float_positional(x_, precision=precision, fractional=False))
x_ = round_to_significant_digits(x_, digits=significant_digits)

if (
not self.allow_extended_input and
Expand Down
33 changes: 23 additions & 10 deletions tests/optimization_problem_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,15 +160,15 @@ def __init__(
self,
transform=None,
has_evaluator=False,
precision=None,
significant_digits=None,
*args,
**kwargs
):
self.test_abs_tol = 0.1
super().__init__('linear_constraints_single_objective', *args, **kwargs)
self.setup_variables(
transform=transform,
precision=precision
significant_digits=significant_digits
)
self.setup_linear_constraints()
if has_evaluator:
Expand All @@ -181,27 +181,27 @@ def __init__(
else:
self.add_objective(self._objective_function)

def setup_variables(self, transform, precision=None):
def setup_variables(self, transform, significant_digits=None):
self.add_variable(
'var_0',
lb=-2,
ub=2,
transform=transform,
precision=precision,
significant_digits=significant_digits,
)
self.add_variable(
'var_1',
lb=-2,
ub=2,
transform=transform,
precision=precision,
significant_digits=significant_digits,
)
self.add_variable(
'var_2',
lb=0,
ub=2,
transform="log",
precision=precision,
significant_digits=significant_digits,
)

def setup_linear_constraints(self):
Expand Down Expand Up @@ -381,10 +381,23 @@ def __init__(
self.setup_linear_constraints()
self.add_objective(self._objective_function)

def setup_variables(self: OptimizationProblem, transform=None, precision=None):
self.add_variable('var_0', lb=-5, ub=5, transform=transform, precision=precision)
self.add_variable('var_1', lb=-5, ub=5, transform=transform, precision=precision)
self.add_variable('var_2', lb=-5, ub=5, transform=transform, precision=precision)
def setup_variables(
self: OptimizationProblem,
transform=None,
significant_digits=None
):
self.add_variable(
'var_0', lb=-5, ub=5,
transform=transform, significant_digits=significant_digits
)
self.add_variable(
'var_1', lb=-5, ub=5,
transform=transform, significant_digits=significant_digits
)
self.add_variable(
'var_2', lb=-5, ub=5,
transform=transform, significant_digits=significant_digits
)

def setup_linear_constraints(self):
self.add_linear_equality_constraint(
Expand Down
Loading

0 comments on commit b415222

Please sign in to comment.