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

Update to allow no bounds #213

Merged
merged 15 commits into from
Feb 29, 2024
Merged
7 changes: 3 additions & 4 deletions examples/scripts/spm_IRPropMin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,10 @@
pybop.Parameter(
"Negative electrode active material volume fraction",
prior=pybop.Gaussian(0.6, 0.05),
bounds=[0.5, 0.8],
),
pybop.Parameter(
"Positive electrode active material volume fraction",
prior=pybop.Gaussian(0.48, 0.05),
bounds=[0.4, 0.7],
),
]

Expand Down Expand Up @@ -52,7 +50,8 @@
pybop.plot_parameters(optim)

# Plot the cost landscape
pybop.plot_cost2d(cost, steps=15)
bounds = np.array([[0.5, 0.8], [0.4, 0.7]])
pybop.plot_cost2d(cost, bounds=bounds, steps=15)

# Plot the cost landscape with optimisation path
pybop.plot_cost2d(cost, optim=optim, steps=15)
pybop.plot_cost2d(cost, optim=optim, bounds=bounds, steps=15)
7 changes: 3 additions & 4 deletions examples/scripts/spm_adam.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,10 @@
pybop.Parameter(
"Negative electrode active material volume fraction",
prior=pybop.Gaussian(0.68, 0.05),
bounds=[0.5, 0.8],
),
pybop.Parameter(
"Positive electrode active material volume fraction",
prior=pybop.Gaussian(0.58, 0.05),
bounds=[0.4, 0.7],
),
]

Expand Down Expand Up @@ -54,7 +52,8 @@
pybop.plot_parameters(optim)

# Plot the cost landscape
pybop.plot_cost2d(cost, steps=15)
bounds = np.array([[0.5, 0.8], [0.4, 0.7]])
pybop.plot_cost2d(cost, bounds=bounds, steps=15)

# Plot the cost landscape with optimisation path
pybop.plot_cost2d(cost, optim=optim, steps=15)
pybop.plot_cost2d(cost, optim=optim, bounds=bounds, steps=15)
7 changes: 3 additions & 4 deletions examples/scripts/spm_descent.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,10 @@
pybop.Parameter(
"Negative electrode active material volume fraction",
prior=pybop.Gaussian(0.68, 0.05),
bounds=[0.5, 0.8],
),
pybop.Parameter(
"Positive electrode active material volume fraction",
prior=pybop.Gaussian(0.58, 0.05),
bounds=[0.4, 0.7],
),
]

Expand Down Expand Up @@ -56,7 +54,8 @@
pybop.plot_parameters(optim)

# Plot the cost landscape
pybop.plot_cost2d(cost, steps=15)
bounds = np.array([[0.5, 0.8], [0.4, 0.7]])
pybop.plot_cost2d(cost, bounds=bounds, steps=15)

# Plot the cost landscape with optimisation path
pybop.plot_cost2d(cost, optim=optim, steps=15)
pybop.plot_cost2d(cost, optim=optim, bounds=bounds, steps=15)
16 changes: 12 additions & 4 deletions pybop/_problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,18 @@ def __init__(
self._target = None

# Set bounds
self.bounds = dict(
lower=[param.bounds[0] for param in self.parameters],
upper=[param.bounds[1] for param in self.parameters],
)
self.bounds = {"lower": [], "upper": []}
count = 0
NicolaCourtier marked this conversation as resolved.
Show resolved Hide resolved
for param in self.parameters:
if param.bounds is not None:
self.bounds["lower"].append(param.bounds[0])
self.bounds["upper"].append(param.bounds[1])
NicolaCourtier marked this conversation as resolved.
Show resolved Hide resolved
else:
self.bounds["lower"].append(None)
self.bounds["upper"].append(None)
count += 1
if count == len(self.parameters):
NicolaCourtier marked this conversation as resolved.
Show resolved Hide resolved
self.bounds = None

# Sample from prior for x0
if x0 is None:
Expand Down
16 changes: 10 additions & 6 deletions pybop/parameters/parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,13 @@ def __init__(
self.initial_value = initial_value
self.value = initial_value
self.bounds = bounds
self.lower_bound = self.bounds[0]
self.upper_bound = self.bounds[1]
self.margin = 1e-4

if self.lower_bound >= self.upper_bound:
raise ValueError("Lower bound must be less than upper bound")
if self.bounds is not None:
self.lower_bound = self.bounds[0]
self.upper_bound = self.bounds[1]
if self.lower_bound >= self.upper_bound:
raise ValueError("Lower bound must be less than upper bound")

def rvs(self, n_samples):
"""
Expand All @@ -67,8 +68,11 @@ def rvs(self, n_samples):
samples = self.prior.rvs(n_samples)

# Constrain samples to be within bounds
offset = self.margin * (self.upper_bound - self.lower_bound)
samples = np.clip(samples, self.lower_bound + offset, self.upper_bound - offset)
if self.bounds is not None:
offset = self.margin * (self.upper_bound - self.lower_bound)
samples = np.clip(
samples, self.lower_bound + offset, self.upper_bound - offset
)

return samples

Expand Down
7 changes: 5 additions & 2 deletions pybop/plotting/plot_cost2d.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ def plot_cost2d(cost, bounds=None, optim=None, steps=10):
If the cost function does not return a valid cost when called with a parameter list.
"""

# Set up parameter bounds
if bounds is None:
# Set up parameter bounds
bounds = get_param_bounds(cost)
else:
bounds = bounds
Expand Down Expand Up @@ -73,7 +73,10 @@ def get_param_bounds(cost):
"""
bounds = np.empty((len(cost.problem.parameters), 2))
for i, param in enumerate(cost.problem.parameters):
bounds[i] = param.bounds
if param.bounds is not None:
bounds[i] = param.bounds
else:
raise ValueError("plot_cost2d could not find bounds required for plotting")
return bounds


Expand Down
9 changes: 9 additions & 0 deletions tests/unit/test_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ def test_parameter_construction(self, parameter):
assert parameter.prior.mean == 0.6
assert parameter.prior.sigma == 0.02
assert parameter.bounds == [0.375, 0.7]
assert parameter.lower_bound == 0.375
assert parameter.upper_bound == 0.7
assert parameter.initial_value == 0.6

@pytest.mark.unit
Expand Down Expand Up @@ -52,6 +54,13 @@ def test_parameter_margin(self, parameter):
parameter.set_margin(margin=1e-3)
assert parameter.margin == 1e-3

@pytest.mark.unit
def test_no_bounds(self):
parameter = pybop.Parameter(
"Negative electrode active material volume fraction",
)
assert parameter.bounds is None

@pytest.mark.unit
def test_invalid_inputs(self, parameter):
# Test error with invalid value
Expand Down
7 changes: 7 additions & 0 deletions tests/unit/test_plots.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ def test_cost_plots(self, cost):
# Plot the cost landscape
pybop.plot_cost2d(cost, steps=5)

# Test without bounds
for param in cost.problem.parameters:
param.bounds = None
with pytest.raises(ValueError):
pybop.plot_cost2d(cost, steps=5)
pybop.plot_cost2d(cost, bounds=np.array([[1e-6, 9e-6], [1e-6, 9e-6]]), steps=5)

@pytest.fixture
def optim(self, cost):
# Define and run an example optimisation
Expand Down
6 changes: 6 additions & 0 deletions tests/unit/test_problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ def test_base_problem(self, parameters, model):
with pytest.raises(ValueError):
pybop._problem.BaseProblem(parameters, model=model, signal=[1e-5, 1e-5])

# Test without bounds
for param in parameters:
param.bounds = None
problem = pybop._problem.BaseProblem(parameters, model=model)
assert problem.bounds is None

@pytest.mark.unit
def test_fitting_problem(self, parameters, dataset, model, signal):
# Test incorrect number of initial parameter values
Expand Down
Loading