Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/bssunfold/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,6 @@
from .unfold_maxed import solve_maxed, unfold_maxed
from .unfold_mcmc import solve_bayesian_mcmc, unfold_mcmc
from .unfold_mlem import solve_mlem, unfold_mlem
from .unfold_nnqp import solve_nnqp, unfold_nnqp
from .unfold_mlem_bs import (
AUTO_BETA_RELATIVE_GRID,
build_bspline_basis,
Expand All @@ -157,6 +156,7 @@
solve_tikhonov_nnls,
unfold_nnksvd,
)
from .unfold_nnqp import solve_nnqp, unfold_nnqp
from .unfold_nsduaz import (
builtin_catalogue,
select_catalogue_initial,
Expand Down
21 changes: 11 additions & 10 deletions src/bssunfold/core/_gnowee.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,8 @@

from __future__ import annotations

import copy as cp
from collections.abc import Callable
from dataclasses import dataclass, field
from dataclasses import dataclass
from math import gamma, sqrt

import numpy as np
Expand Down Expand Up @@ -316,7 +315,8 @@ def initialize(self, num_samples: int, method: str | None = None) -> np.ndarray:
elif method in ("lhc", "lhs"):
try:
from scipy.stats.qmc import LatinHypercube
sampler = LatinHypercube(d=n_dim, seed=int(self.rng.integers(0, 2**31 - 1)))
seed_int = int(self.rng.integers(0, 2**31 - 1))
sampler = LatinHypercube(d=n_dim, seed=seed_int)
unit = sampler.random(n=num_samples)
except Exception:
# Fallback: crude Latin-hypercube via permutation per dimension
Expand Down Expand Up @@ -442,7 +442,8 @@ def mutate(self, pop: list[Parent]) -> list[np.ndarray]:
perm1 = self.rng.permutation(n)
perm2 = self.rng.permutation(n)
r = float(self.rng.random())
k = self.rng.random((n, dim)) > (self.s.frac_mutation * float(self.rng.random()))
k = self.rng.random((n, dim)) > (
self.s.frac_mutation * float(self.rng.random()))
diff = pop_arr[perm1] - pop_arr[perm2]
step = r * diff
children = pop_arr + step * k
Expand All @@ -456,7 +457,8 @@ def population_update(self, parents: list[Parent], children: list[np.ndarray],
timeline: list[Event] | None = None,
adopted_parents: list[int] | None = None,
mh_frac: float = 0.0,
random_parents: bool = False) -> tuple[list[Parent], int, list[Event] | None]:
random_parents: bool = False,
) -> tuple[list[Parent], int, list[Event] | None]:
"""Evaluate children and replace worse parents (port of ``population_update``).

Keeps the parents sorted ascending by fitness. Implements:
Expand All @@ -474,7 +476,6 @@ def population_update(self, parents: list[Parent], children: list[np.ndarray],

replace = 0
feval = 0
worst = max((p.fitness for p in parents), default=self.s.penalty)
for i, child_vars in enumerate(children):
fnew = float(self.objective(np.asarray(child_vars, dtype=float)))
if fnew > self.s.penalty:
Expand Down Expand Up @@ -551,7 +552,8 @@ def run_gnowee(lb: np.ndarray, ub: np.ndarray,
settings: GnoweeSettings | None = None,
rng: np.random.Generator | None = None,
seed_solution: np.ndarray | None = None,
extra_starting: np.ndarray | None = None) -> tuple[np.ndarray, float, list[Event]]:
extra_starting: np.ndarray | None = None,
) -> tuple[np.ndarray, float, list[Event]]:
"""Run the Gnowee optimizer on a continuous problem.

Parameters
Expand Down Expand Up @@ -606,8 +608,6 @@ def run_gnowee(lb: np.ndarray, ub: np.ndarray,
timeline.append(Event(0, len(pop), pop[0].fitness,
np.array(pop[0].variables, dtype=float)))

fe = gh.s.frac_elite
fl = gh.s.frac_levy
converge = False
while not converge:
# Gnowee re-samples the elite/levy fractions each generation for MI
Expand Down Expand Up @@ -669,7 +669,8 @@ def run_gnowee(lb: np.ndarray, ub: np.ndarray,
converge = True
if settings.verbose:
print("Gnowee: fitness convergence (absolute).")
elif abs((pop[0].fitness - settings.optimum) / settings.optimum) <= settings.opt_conv_tol:
elif (abs((pop[0].fitness - settings.optimum) / settings.optimum)
<= settings.opt_conv_tol):
converge = True
if settings.verbose:
print("Gnowee: fitness convergence (relative).")
Expand Down
2 changes: 1 addition & 1 deletion src/bssunfold/core/detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
from .unfold_mystic import unfold_mystic as unfold_mystic_impl
from .unfold_mystic import unfold_mystic_hybrid as unfold_mystic_hybrid_impl
from .unfold_nnksvd import unfold_nnksvd as unfold_nnksvd_impl
from .unfold_nnqp import unfold_nnqp as unfold_nnqp_impl
from .unfold_nsduaz import unfold_nsduaz as unfold_nsduaz_impl
from .unfold_nspline import unfold_nspline as unfold_nspline_impl
from .unfold_odl_advanced import (
Expand All @@ -116,7 +117,6 @@
from .unfold_pspline_reml import (
unfold_pspline_reml as unfold_pspline_reml_impl,
)
from .unfold_nnqp import unfold_nnqp as unfold_nnqp_impl
from .unfold_qpmad import unfold_qpmad as unfold_qpmad_impl
from .unfold_qpsolvers import unfold_qpsolvers as unfold_qpsolvers_impl
from .unfold_qubo import unfold_qubo as unfold_qubo_impl
Expand Down
3 changes: 2 additions & 1 deletion src/bssunfold/core/unfold_gnowee.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ def _build_seed(A: np.ndarray, b: np.ndarray, x0: np.ndarray | None) -> np.ndarr
return seed


def _build_log_bounds(seed: np.ndarray, half_range: float) -> tuple[np.ndarray, np.ndarray]:
def _build_log_bounds(seed: np.ndarray, half_range: float,
) -> tuple[np.ndarray, np.ndarray]:
"""Return ``(lb, ub)`` in log space centred on the seed."""
y0 = np.log(np.maximum(np.asarray(seed, dtype=float), 1e-300))
span = half_range * np.log(10.0)
Expand Down
21 changes: 14 additions & 7 deletions src/bssunfold/core/unfold_qpmad.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
from typing import Any

import numpy as np
from scipy.linalg import cho_factor, cho_solve, solve_triangular
from scipy.linalg import solve_triangular

from ..logging_config import get_logger
from ._base_unfolder import make_solve_wrapper, run_unfolding
Expand Down Expand Up @@ -154,11 +154,15 @@ def solve_H(rhs: np.ndarray) -> np.ndarray:
ub_arr = np.asarray(ub, dtype=float)
for i in range(n):
if np.isfinite(lb_arr[i]):
r = np.zeros(n); r[i] = 1.0
rows_C.append(r); vals_d.append(float(lb_arr[i]))
r = np.zeros(n)
r[i] = 1.0
rows_C.append(r)
vals_d.append(float(lb_arr[i]))
if np.isfinite(ub_arr[i]):
r = np.zeros(n); r[i] = -1.0
rows_C.append(r); vals_d.append(-float(ub_arr[i]))
r = np.zeros(n)
r[i] = -1.0
rows_C.append(r)
vals_d.append(-float(ub_arr[i]))

if A is not None and lb_A is not None and ub_A is not None:
A_arr = np.asarray(A, dtype=float)
Expand Down Expand Up @@ -396,7 +400,8 @@ def _solve_qp_qpmad_cpp(
)
return np.asarray(x, dtype=float), "OK" if status == 0 else "INFEASIBLE"
except Exception as e:
logger.warning(f"qpmad.solve call failed: {e}; falling back to python backend")
logger.warning(
f"qpmad.solve call failed: {e}; falling back to python backend")

if hasattr(qpmad_module, "Solver"):
# OOP form: solver = qpmad.Solver(); solver.solve(...)
Expand All @@ -416,7 +421,9 @@ def _solve_qp_qpmad_cpp(
return np.zeros(n), "INFEASIBLE"
return np.asarray(x, dtype=float), "OK" if status == 0 else "INFEASIBLE"
except Exception as e:
logger.warning(f"qpmad.Solver.solve call failed: {e}; falling back to python backend")
logger.warning(
f"qpmad.Solver.solve call failed: {e};"
" falling back to python backend")

raise ImportError(
"qpmad Python bindings found but no compatible solve API. "
Expand Down
3 changes: 2 additions & 1 deletion tests/test_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -2546,7 +2546,8 @@ def _make_detector(self):

@pytest.mark.parametrize(
"method",
["cvxpy", "qpsolvers", "mlem", "landweber", "genetic", "gnowee", "nnqp", "qpmad"],
["cvxpy", "qpsolvers", "mlem", "landweber", "genetic",
"gnowee", "nnqp", "qpmad"],
)
def test_spectrum_zero_above_emax(self, method):
det = self._make_detector()
Expand Down
1 change: 0 additions & 1 deletion tests/test_gnowee.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
from bssunfold.core._gnowee import (
GnoweeHeuristics,
GnoweeSettings,
Parent,
levy,
rejection_bounds,
run_gnowee,
Expand Down
Loading