Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Respect prob.bounds in all solver wrappers #463

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
16 changes: 8 additions & 8 deletions src/simsopt/_core/optimizable.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,8 +396,8 @@ def full_lower_bounds(self, lower_bounds: RealArray) -> None:
Args:
lower_bounds: Lower bounds of the DOFs
"""
if len(self.lower_bounds) != len(lower_bounds):
raise DofLengthMismatchError(len(lower_bounds), len(self.lower_bounds))
if len(self._lower_bounds) != len(lower_bounds):
raise DofLengthMismatchError(len(lower_bounds), len(self._lower_bounds))
self._lower_bounds = np.asarray(lower_bounds, dtype=np.double)

@property
Expand Down Expand Up @@ -434,8 +434,8 @@ def full_upper_bounds(self, upper_bounds: RealArray) -> None:
Args:
upper_bounds: Upper bounds of the DOFs
"""
if len(self.upper_bounds) != len(upper_bounds):
raise DofLengthMismatchError(len(upper_bounds), len(self.upper_bounds))
if len(self._upper_bounds) != len(upper_bounds):
raise DofLengthMismatchError(len(upper_bounds), len(self._upper_bounds))
self._upper_bounds = np.asarray(upper_bounds, dtype=np.double)

@property
Expand Down Expand Up @@ -1197,9 +1197,9 @@ def full_lower_bounds(self, lb) -> None:
Set the lower bounds of the fixed and free DOFS associated with the
current Optimizable object and its ancestors.
"""
if list(self.dof_indices.values())[-1][-1] != len(lb):
if list(self._full_dof_indices.values())[-1][-1] != len(lb):
raise ValueError
for opt, indices in self.dof_indices.items():
for opt, indices in self._full_dof_indices.items():
opt._dofs.full_lower_bounds = lb[indices[0]:indices[1]]

@property
Expand Down Expand Up @@ -1278,9 +1278,9 @@ def full_upper_bounds(self, ub) -> None:
Set the upper bounds of the fixed and free DOFS associated with the
current Optimizable object and its ancestors.
"""
if list(self.dof_indices.values())[-1][-1] != len(ub):
if list(self._full_dof_indices.values())[-1][-1] != len(ub):
raise ValueError
for opt, indices in self.dof_indices.items():
for opt, indices in self._full_dof_indices.items():
opt._dofs.full_upper_bounds = ub[indices[0]:indices[1]]

@property
Expand Down
15 changes: 12 additions & 3 deletions src/simsopt/solve/mpi.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,16 @@ def _f_proc0(x):
logger.debug(f"residuals are {residuals}")
return residuals


if "bounds" in kwargs:
import warnings
warnings.warn("The bounds argument is being deprecated, \
please use prob.bounds instead.", DeprecationWarning, 2)
else:
# Only specify the bounds argument if they are finite
if np.isfinite(prob.lower_bounds).any() or np.isfinite(prob.upper_bounds).any():
kwargs['bounds'] = prob.bounds

# For MPI finite difference gradient, get the worker and leader action from
# MPIFiniteDifference
if grad:
Expand All @@ -211,10 +221,9 @@ def _f_proc0(x):
logger.info("Using finite difference method implemented in "
"SIMSOPT for evaluating gradient")
try:
result = least_squares(_f_proc0, x0, jac=fd.jac, verbose=2,
**kwargs)
result = least_squares(_f_proc0, x0, jac=fd.jac, verbose=2, **kwargs)
except:
print("Failure on proc0_world")
logger.error("Failure on proc0_world")
result = Struct()
result.x = x0

Expand Down
22 changes: 20 additions & 2 deletions src/simsopt/solve/serial.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,22 @@ def objective(x):
nevals += 1
return residuals

if "bounds" in kwargs:
import warnings
warnings.warn("The bounds argument is being deprecated, \
please use prob.bounds instead.", DeprecationWarning, 2)
else:
# Only specify the bounds argument if they are finite
if np.isfinite(prob.lower_bounds).any() or np.isfinite(prob.upper_bounds).any():
kwargs['bounds'] = prob.bounds

logger.info("Beginning solve.")
#if grad is None:
# grad = prob.dofs.grad_avail

#if not 'verbose' in kwargs:

print('prob is ', prob)
logger.info('prob is {}'.format(prob))
x0 = np.copy(prob.x)
if grad:
fd = FiniteDifference(prob.residuals, abs_step=abs_step,
Expand All @@ -156,7 +165,7 @@ def objective(x):
result = least_squares(objective, x0, verbose=2, jac=fd.jac, **kwargs)
else:
logger.info("Using derivative-free method")
result = least_squares(objective, x0, verbose=2, **kwargs)
result = least_squares(objective, x0,verbose=2, **kwargs)

datalogging_started = False
objective_file.close()
Expand Down Expand Up @@ -249,6 +258,15 @@ def objective(x):

#if not 'verbose' in kwargs:

# if "bounds" in kwargs:
# import warnings
# warnings.warn("The bounds argument is being deprecated, \
# please use prob.bounds instead.", DeprecationWarning, 2)
# else:
# # Only specify the bounds argument if they are finite
# if np.isfinite(prob.lower_bounds).any() or np.isfinite(prob.upper_bounds).any():
# kwargs['bounds'] = prob.bounds

logger.info("Beginning solve.")
x0 = np.copy(prob.x)
if grad:
Expand Down
38 changes: 37 additions & 1 deletion tests/core/test_optimizable.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def sum(self):
return np.sum(self.local_full_x)

def product(self):
return np.product(self.local_full_x)
return np.prod(self.local_full_x)

return_fn_map = {'sum': sum, 'prod': product}

Expand Down Expand Up @@ -845,12 +845,48 @@ def test_full_bounds(self):
def test_lower_bounds(self):
pass

def test_full_lower_bounds(self):
iden = Identity(x=10, dof_fixed=False)
adder = Adder(n=3, x0=[1, 2, 3], names=['x', 'y', 'z'])
adder.fix('y')
opt = iden + adder
# ['Adder42:x', 'Adder42:y', 'Adder42:z', 'Identity29:x0']
opt.full_lower_bounds = np.array([1, 2, 3, 4])
self.assertTrue(np.allclose(opt.lower_bounds, np.array([1, 3, 4])))
self.assertTrue(np.allclose(adder.lower_bounds, np.array([1, 3])))
self.assertTrue(np.allclose(adder.full_lower_bounds, np.array([1, 2, 3])))
self.assertTrue(np.allclose(iden.lower_bounds, np.array([4])))
self.assertTrue(np.allclose(iden.lower_bounds, iden.full_lower_bounds))
self.assertTrue(np.allclose(opt.full_bounds[0], opt.full_lower_bounds))
with self.assertRaises(ValueError): # Too few elements
opt.full_lower_bounds = np.array([1, 2, 3])
with self.assertRaises(ValueError): # Too many elements
opt.full_lower_bounds = np.array([1, 2, 3, 4, 5])

def test_local_lower_bounds(self):
pass

def test_upper_bounds(self):
pass

def test_full_upper_bounds(self):
iden = Identity(x=10, dof_fixed=False)
adder = Adder(n=3, x0=[1, 2, 3], names=['x', 'y', 'z'])
adder.fix('y')
opt = iden + adder
# ['Adder42:x', 'Adder42:y', 'Adder42:z', 'Identity29:x0']
opt.full_upper_bounds = np.array([1, 2, 3, 4])
self.assertTrue(np.allclose(opt.upper_bounds, np.array([1, 3, 4])))
self.assertTrue(np.allclose(adder.upper_bounds, np.array([1, 3])))
self.assertTrue(np.allclose(adder.full_upper_bounds, np.array([1, 2, 3])))
self.assertTrue(np.allclose(iden.upper_bounds, np.array([4])))
self.assertTrue(np.allclose(iden.upper_bounds, iden.full_upper_bounds))
self.assertTrue(np.allclose(opt.full_bounds[1], opt.full_upper_bounds))
with self.assertRaises(ValueError): # Too few elements
opt.full_upper_bounds = np.array([1, 2, 3])
with self.assertRaises(ValueError): # Too many elements
opt.full_upper_bounds = np.array([1, 2, 3, 4, 5])

def test_local_upper_bounds(self):
pass

Expand Down
27 changes: 27 additions & 0 deletions tests/solve/test_least_squares.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,33 @@ def test_solve_quadratic(self):
self.assertTrue(np.allclose(iden2.x, [2]))
self.assertTrue(np.allclose(iden3.x, [3]))

def test_solve_quadratic_bounds(self):
"""
Same as test_solve_quadratic, except with different bounds. The solver
should therefore run into the bounds instead of the regular minimum.
"""
with ScratchDir("."):
for solver in solvers:
# Check that bounds via problem and bounds argument behave the same
for bounds_type in ['argument', 'property']:
iden1 = Identity()
iden2 = Identity()
iden3 = Identity()
term1 = (iden1.f, 1, 1)
term2 = (iden2.f, 2, 2)
term3 = (iden3.f, 3, 3)
prob = LeastSquaresProblem.from_tuples([term1, term2, term3])
if bounds_type == 'argument':
solver(prob, bounds=([-np.inf, -np.inf, -np.inf], [np.inf, 1, 10]))
else:
prob.upper_bounds = [np.inf, 1, 10]
solver(prob)
print("assertAlmostEqual", prob.objective(), prob.x)
self.assertAlmostEqual(prob.objective(), 2)
self.assertTrue(np.allclose(iden1.x, [1]))
self.assertTrue(np.allclose(iden2.x, [1]))
self.assertTrue(np.allclose(iden3.x, [3]))

def test_solve_quadratic_fixed(self):
"""
Same as test_solve_quadratic, except with different weights and x
Expand Down
Loading