Source code for pyxu.abc.solver

  1import collections.abc as cabc
  2import datetime as dt
  3import enum
  4import logging
  5import operator
  6import pathlib as plib
  7import shutil
  8import sys
  9import tempfile
 10import threading
 11import typing as typ
 12
 13import numpy as np
 14
 15import pyxu.info.ptype as pxt
 16import pyxu.util as pxu
 17
 18__all__ = [
 19    "SolverMode",
 20    "Solver",
 21    "StoppingCriterion",
 22]
 23
 24
[docs] 25@enum.unique 26class SolverMode(enum.Enum): 27 """ 28 Solver execution mode. 29 """ 30 31 BLOCK = enum.auto() 32 MANUAL = enum.auto() 33 ASYNC = enum.auto()
34 35
[docs] 36class StoppingCriterion: 37 """ 38 State machines (SM) which decide when to stop iterative solvers by examining their mathematical state. 39 40 SM decisions are always accompanied by at least one numerical statistic. These stats may be queried by solvers via 41 :py:meth:`~pyxu.abc.StoppingCriterion.info` to provide diagnostic information to users. 42 43 Composite stopping criteria can be implemented via the overloaded (and[``&``], or[``|``]) operators. 44 """ 45
[docs] 46 def stop(self, state: cabc.Mapping[str]) -> bool: 47 """ 48 Compute a stop signal based on the current mathematical state. 49 50 Parameters 51 ---------- 52 state: ~collections.abc.Mapping 53 Full mathematical state of solver at some iteration, i.e. :py:attr:`~pyxu.abc.Solver._mstate`. 54 55 Values from `state` may be cached inside the instance to form complex stopping conditions. 56 57 Returns 58 ------- 59 s: bool 60 True if no further iterations should be performed, False otherwise. 61 """ 62 raise NotImplementedError
63
[docs] 64 def info(self) -> cabc.Mapping[str, float]: 65 """ 66 Get statistics associated with the last call to :py:meth:`~pyxu.abc.StoppingCriterion.stop`. 67 68 Returns 69 ------- 70 data: ~collections.abc.Mapping 71 """ 72 raise NotImplementedError
73
[docs] 74 def clear(self): 75 """ 76 Clear SM state (if any). 77 78 This method is useful when a :py:class:`~pyxu.abc.StoppingCriterion` instance must be reused in another call to 79 :py:meth:`~pyxu.abc.Solver.fit`. 80 """ 81 pass
82 83 def __or__(self, other: "StoppingCriterion") -> "StoppingCriterion": 84 return _StoppingCriteriaComposition(lhs=self, rhs=other, op=operator.or_) 85 86 def __and__(self, other: "StoppingCriterion") -> "StoppingCriterion": 87 return _StoppingCriteriaComposition(lhs=self, rhs=other, op=operator.and_)
88 89 90class _StoppingCriteriaComposition(StoppingCriterion): 91 def __init__( 92 self, 93 lhs: "StoppingCriterion", 94 rhs: "StoppingCriterion", 95 op: cabc.Callable[[bool, bool], bool], 96 ): 97 self._lhs = lhs 98 self._rhs = rhs 99 self._op = op 100 101 def stop(self, state: cabc.Mapping) -> bool: 102 return self._op(self._lhs.stop(state), self._rhs.stop(state)) 103 104 def info(self) -> cabc.Mapping[str, float]: 105 return {**self._lhs.info(), **self._rhs.info()} 106 107 def clear(self): 108 self._lhs.clear() 109 self._rhs.clear() 110 111
[docs] 112class Solver: 113 r""" 114 Iterative solver for minimization problems of the form :math:`\hat{x} = \arg\min_{x \in \mathbb{R}^{M_{1} 115 \times\cdots\times M_{D}}} \mathcal{F}(x)`, where the form of :math:`\mathcal{F}` is solver-dependent. 116 117 Solver provides a versatile API for solving optimisation problems, with the following features: 118 119 * manual/automatic/background execution of solver iterations via parameters provided to 120 :py:meth:`~pyxu.abc.Solver.fit`. (See below.) 121 * automatic checkpointing of solver progress, providing a safe restore point in case of faulty numerical code. Each 122 solver instance backs its state and final output to a folder on disk for post-analysis. In particular 123 :py:meth:`~pyxu.abc.Solver.fit` should never crash: detailed exception information will always be available in a 124 logfile for post-analysis. 125 * arbitrary specification of complex stopping criteria via the :py:class:`~pyxu.abc.StoppingCriterion` class. 126 * solve for multiple initial points in parallel. (Not always supported by all solvers.) 127 128 To implement a new iterative solver, users need to sub-class :py:class:`~pyxu.abc.Solver` and overwrite the methods 129 below: 130 131 * :py:meth:`~pyxu.abc.Solver.__init__` 132 * :py:meth:`~pyxu.abc.Solver.m_init` [i.e. math-init()] 133 * :py:meth:`~pyxu.abc.Solver.m_step` [i.e. math-step()] 134 * :py:meth:`~pyxu.abc.Solver.default_stop_crit` [optional; see method definition for details] 135 * :py:meth:`~pyxu.abc.Solver.objective_func` [optional; see method definition for details] 136 137 Advanced functionalities of :py:class:`~pyxu.abc.Solver` are automatically inherited by sub-classes. 138 139 140 Examples 141 -------- 142 Here are examples on how to solve minimization problems with this class: 143 144 .. code-block:: python3 145 146 slvr = Solver() 147 148 ### 1. Blocking mode: .fit() does not return until solver has stopped. 149 >>> slvr.fit(mode=SolverMode.BLOCK, ...) 150 >>> data, hist = slvr.stats() # final output of solver. 151 152 ### 2. Async mode: solver iterations run in the background. 153 >>> slvr.fit(mode=SolverMode.ASYNC, ...) 154 >>> print('test') # you can do something in between. 155 >>> slvr.busy() # or check whether the solver already stopped. 156 >>> slvr.stop() # and preemptively force it to stop. 157 >>> data, hist = slvr.stats() # then query the result after a (potential) force-stop. 158 159 ### 3. Manual mode: fine-grain control of solver data per iteration. 160 >>> slvr.fit(mode=SolverMode.MANUAL, ...) 161 >>> for data in slvr.steps(): 162 ... # Do something with the logged variables after each iteration. 163 ... pass # solver has stopped after the loop. 164 >>> data, hist = slvr.stats() # final output of solver. 165 """ 166 167 _mstate: dict[str, typ.Any] #: Mathematical state. 168 _astate: dict[str, typ.Any] #: Book-keeping (non-math) state. 169
[docs] 170 def __init__( 171 self, 172 *, 173 folder: pxt.Path = None, 174 exist_ok: bool = False, 175 stop_rate: pxt.Integer = 1, 176 writeback_rate: pxt.Integer = None, 177 verbosity: pxt.Integer = None, 178 show_progress: bool = True, 179 log_var: pxt.VarName = frozenset(), 180 ): 181 """ 182 Parameters 183 ---------- 184 folder: Path 185 Directory on disk where instance data should be stored. A location will be automatically chosen if 186 unspecified. (Default: OS-dependent tempdir.) 187 exist_ok: bool 188 If `folder` is specified and `exist_ok` is false (default), :py:class:`FileExistsError` is raised if the 189 target directory already exists. 190 stop_rate: Integer 191 Rate at which solver evaluates stopping criteria. 192 writeback_rate: Integer 193 Rate at which solver checkpoints are written to disk: 194 195 - If `None` (default), all checkpoints are disabled: the final solver output is only stored in memory. 196 - If `0`, intermediate checkpoints are disabled: only the final solver output will be written to disk. 197 - Any other integer: checkpoint to disk at provided interval. Must be a multiple of `stop_rate`. 198 verbosity: Integer 199 Rate at which stopping criteria statistics are logged. Must be a multiple of `stop_rate`. Defaults to 200 `stop_rate` if unspecified. 201 show_progress: bool 202 If True (default) and :py:meth:`~pyxu.abc.Solver.fit` is run with mode=BLOCK, then statistics are also 203 logged to stdout. 204 log_var: VarName 205 Variables from the solver's math-state (:py:attr:`~pyxu.abc.Solver._mstate`) to be logged per iteration. 206 These are the variables made available when calling :py:meth:`~pyxu.abc.Solver.stats`. 207 208 Notes 209 ----- 210 * Partial device<>CPU synchronization takes place when stopping-criteria are evaluated. Increasing `stop_rate` 211 is advised to reduce the effect of such transfers when applicable. 212 * Full device<>CPU synchronization takes place at checkpoint-time. Increasing `writeback_rate` is advised to 213 reduce the effect of such transfers when applicable. 214 """ 215 self._mstate = dict() 216 self._astate = dict( 217 history=None, # stopping criteria values per iteration 218 idx=0, # iteration index 219 log_rate=None, 220 log_var=None, 221 logger=None, 222 stdout=None, 223 stop_crit=None, 224 stop_rate=None, 225 track_objective=None, 226 wb_rate=None, 227 workdir=None, 228 # Execution-mode related ----------- 229 mode=None, 230 active=None, 231 worker=None, 232 ) 233 234 try: 235 if folder is None: 236 folder = plib.Path(tempfile.mkdtemp(prefix="pyxu_")) 237 elif (folder := plib.Path(folder).expanduser().resolve()).exists() and (not exist_ok): 238 raise FileExistsError(f"{folder} already exists.") 239 else: 240 shutil.rmtree(folder, ignore_errors=True) 241 folder.mkdir(parents=True) 242 self._astate["workdir"] = folder 243 except Exception: 244 raise Exception(f"folder: expected path-like, got {type(folder)}.") 245 246 try: 247 assert stop_rate >= 1 248 self._astate["stop_rate"] = int(stop_rate) 249 except Exception: 250 raise ValueError(f"stop_rate must be positive, got {stop_rate}.") 251 252 try: 253 if writeback_rate is None: # no checkpoints 254 pass 255 elif writeback_rate == 0: # final checkpoint only 256 pass 257 else: # regular checkpoints 258 assert writeback_rate > 0 259 assert writeback_rate % self._astate["stop_rate"] == 0 260 writeback_rate = int(writeback_rate) 261 self._astate["wb_rate"] = writeback_rate 262 except Exception: 263 raise ValueError(f"writeback_rate must be (None, 0, <multiple of stop_rate>), got {writeback_rate}.") 264 265 try: 266 if verbosity is None: 267 verbosity = self._astate["stop_rate"] 268 assert verbosity % self._astate["stop_rate"] == 0 269 self._astate["log_rate"] = int(verbosity) 270 self._astate["stdout"] = bool(show_progress) 271 except Exception: 272 raise ValueError(f"verbosity must be a multiple of stop_rate({stop_rate}), got {verbosity}.") 273 274 try: 275 if isinstance(log_var, str): 276 log_var = (log_var,) 277 self._astate["log_var"] = frozenset(log_var) 278 except Exception: 279 raise ValueError(f"log_var: expected collection, got {type(log_var)}.")
280
[docs] 281 def fit(self, **kwargs): 282 r""" 283 Solve minimization problem(s) defined in :py:meth:`~pyxu.abc.Solver.__init__`, with the provided run-specifc 284 parameters. 285 286 Parameters 287 ---------- 288 kwargs 289 See class-level docstring for class-specific keyword parameters. 290 stop_crit: StoppingCriterion 291 Stopping criterion to end solver iterations. If unspecified, defaults to 292 :py:meth:`~pyxu.abc.Solver.default_stop_crit`. 293 mode: SolverMode 294 Execution mode. 295 See :py:class:`~pyxu.abc.Solver` for usage examples. 296 297 Useful method pairs depending on the execution mode: 298 299 * BLOCK: :py:meth:`~pyxu.abc.Solver.fit` 300 * ASYNC: :py:meth:`~pyxu.abc.Solver.fit`, :py:meth:`~pyxu.abc.Solver.busy`, :py:meth:`~pyxu.abc.Solver.stop` 301 * MANUAL: :py:meth:`~pyxu.abc.Solver.fit`, :py:meth:`~pyxu.abc.Solver.steps` 302 track_objective: bool 303 Auto-compute objective function every time stopping criterion is evaluated. 304 """ 305 self._fit_init( 306 mode=kwargs.pop("mode", SolverMode.BLOCK), 307 stop_crit=kwargs.pop("stop_crit", None), 308 track_objective=kwargs.pop("track_objective", False), 309 ) 310 self.m_init(**kwargs) 311 self._fit_run()
312
[docs] 313 def m_init(self, **kwargs): 314 """ 315 Set solver's initial mathematical state based on kwargs provided to :py:meth:`~pyxu.abc.Solver.fit`. 316 317 This method must only manipulate :py:attr:`~pyxu.abc.Solver._mstate`. 318 319 After calling this method, the solver must be able to complete its 1st iteration via a call to 320 :py:meth:`~pyxu.abc.Solver.m_step`. 321 """ 322 raise NotImplementedError
323
[docs] 324 def m_step(self): 325 """ 326 Perform one (mathematical) step. 327 328 This method must only manipulate :py:attr:`~pyxu.abc.Solver._mstate`. 329 """ 330 raise NotImplementedError
331
[docs] 332 def steps(self, n: pxt.Integer = None) -> cabc.Generator: 333 """ 334 Generator of logged variables after each iteration. 335 336 The i-th call to :py:func:`next` on this object returns the logged variables after the i-th solver iteration. 337 338 This method is only usable after calling :py:meth:`~pyxu.abc.Solver.fit` with mode=MANUAL. See 339 :py:class:`~pyxu.abc.Solver` for usage examples. 340 341 There is no guarantee that a checkpoint on disk exists when the generator is exhausted. (Reason: potential 342 exceptions raised during solver's progress.) Users should invoke :py:meth:`~pyxu.abc.Solver.writeback` 343 afterwards if needed. 344 345 Parameters 346 ---------- 347 n: Integer 348 Maximum number of :py:func:`next` calls allowed before exhausting the generator. Defaults to infinity if 349 unspecified. 350 351 The generator will terminate prematurely if the solver naturally stops before `n` calls to :py:func:`next` 352 are made. 353 """ 354 self._check_mode(SolverMode.MANUAL) 355 i = 0 356 while (n is None) or (i < n): 357 if self._step(): 358 data, _ = self.stats() 359 yield data 360 i += 1 361 else: 362 self._astate["mode"] = None # force steps() to be call-once when exhausted. 363 self._cleanup_logger() 364 return
365 366 _stats_data_spec = dict[str, typ.Union[pxt.Real, pxt.NDArray, None]] 367 _stats_history_spec = typ.Union[np.ndarray, None] 368
[docs] 369 def stats(self) -> tuple[_stats_data_spec, _stats_history_spec]: 370 """ 371 Query solver state. 372 373 Returns 374 ------- 375 data: ~collections.abc.Mapping 376 Value(s) of ``log_var`` (s) after last iteration. 377 history: numpy.ndarray, None 378 (N_iter,) records of stopping-criteria values sampled every `stop_rate` iteration. 379 380 Notes 381 ----- 382 If any of the ``log_var`` (s) and/or ``history`` are not (yet) known at query time, ``None`` is returned. 383 """ 384 history = self._astate["history"] 385 if history is not None: 386 if len(history) > 0: 387 history = np.concatenate(history, dtype=history[0].dtype, axis=0) 388 else: 389 history = None 390 data = {k: self._mstate.get(k) for k in self._astate["log_var"]} 391 return data, history
392 393 @property 394 def workdir(self) -> pxt.Path: 395 """ 396 Returns 397 ------- 398 wd: Path 399 Absolute path to the directory on disk where instance data is stored. 400 """ 401 return self._astate["workdir"] 402 403 @property 404 def logfile(self) -> pxt.Path: 405 """ 406 Returns 407 ------- 408 lf: Path 409 Absolute path to the log file on disk where stopping criteria statistics are logged. 410 """ 411 return self.workdir / "solver.log" 412 413 @property 414 def datafile(self) -> pxt.Path: 415 """ 416 Returns 417 ------- 418 df: Path 419 Absolute path to the file on disk where ``log_var`` (s) are stored during checkpointing or after solver has 420 stopped. 421 """ 422 return self.workdir / "data.zarr" 423
[docs] 424 def busy(self) -> bool: 425 """ 426 Test if an async-running solver has stopped. 427 428 This method is only usable after calling :py:meth:`~pyxu.abc.Solver.fit` with mode=ASYNC. See 429 :py:class:`~pyxu.abc.Solver` for usage examples. 430 431 Returns 432 ------- 433 b: bool 434 True if solver has stopped, False otherwise. 435 """ 436 self._check_mode(SolverMode.ASYNC, SolverMode.BLOCK) 437 return self._astate["active"].is_set()
438
[docs] 439 def solution(self): 440 """ 441 Output the "solution" of the optimization problem. 442 443 This is a helper method intended for novice users. The return type is sub-class dependent, so don't write an 444 API using this: use :py:meth:`~pyxu.abc.Solver.stats` instead. 445 """ 446 raise NotImplementedError
447
[docs] 448 def stop(self): 449 """ 450 Stop an async-running solver. 451 452 This method is only usable after calling :py:meth:`~pyxu.abc.Solver.fit` with mode=ASYNC. See 453 :py:class:`~pyxu.abc.Solver` for usage examples. 454 455 This method will block until the solver has stopped. 456 457 There is no guarantee that a checkpoint on disk exists once halted. (Reason: potential exceptions raised during 458 solver's progress.) Users should invoke :py:meth:`~pyxu.abc.Solver.writeback` afterwards if needed. 459 460 Users must call this method to terminate an async-solver, even if :py:meth:`~pyxu.abc.Solver.busy` is False. 461 """ 462 self._check_mode(SolverMode.ASYNC, SolverMode.BLOCK) 463 self._astate["active"].clear() 464 self._astate["worker"].join() 465 self._astate.update( 466 mode=None, # forces stop() to be call-once. 467 active=None, 468 worker=None, 469 ) 470 self._cleanup_logger()
471 472 def _fit_init( 473 self, 474 mode: SolverMode, 475 stop_crit: StoppingCriterion, 476 track_objective: bool, 477 ): 478 def _init_logger(): 479 log_name = str(self.workdir) 480 logger = logging.getLogger(log_name) 481 logger.handlers.clear() 482 logger.setLevel("DEBUG") 483 484 fmt = logging.Formatter(fmt="{levelname} -- {message}", style="{") 485 handler = [logging.FileHandler(self.logfile, mode="w")] 486 if (mode is SolverMode.BLOCK) and self._astate["stdout"]: 487 handler.append(logging.StreamHandler(sys.stdout)) 488 for h in handler: 489 h.setLevel("DEBUG") 490 h.setFormatter(fmt) 491 logger.addHandler(h) 492 493 return logger 494 495 self._mstate.clear() 496 497 if stop_crit is None: 498 stop_crit = self.default_stop_crit() 499 stop_crit.clear() 500 501 if track_objective: 502 from pyxu.opt.stop import Memorize 503 504 stop_crit |= Memorize(var="objective_func") 505 506 self._astate.update( # suitable state for a new call to fit(). 507 history=[], 508 idx=0, 509 logger=_init_logger(), 510 stop_crit=stop_crit, 511 track_objective=track_objective, 512 mode=mode, 513 active=None, 514 worker=None, 515 ) 516 517 def _fit_run(self): 518 self._m_persist() 519 520 mode = self._astate["mode"] 521 if mode is SolverMode.MANUAL: 522 # User controls execution via steps(). 523 pass 524 else: # BLOCK / ASYNC 525 self._astate.update( 526 active=threading.Event(), 527 worker=Solver._Worker(self), 528 ) 529 self._astate["active"].set() 530 self._astate["worker"].start() 531 if mode is SolverMode.BLOCK: 532 self._astate["worker"].join() 533 self.stop() # state clean-up 534 else: 535 # User controls execution via busy() + stop(). 536 pass 537
[docs] 538 def writeback(self): 539 """ 540 Checkpoint state to disk. 541 """ 542 data, history = self.stats() 543 544 pxu.save_zarr(self.datafile, {"history": history, **data})
545 546 def _check_mode(self, *modes: SolverMode): 547 m = self._astate["mode"] 548 if m in modes: 549 pass # ok 550 else: 551 if m is None: 552 msg = "Illegal method call: invoke Solver.fit() first." 553 else: 554 msg = " ".join( 555 [ 556 "Illegal method call: can only be used if Solver.fit() invoked with", 557 "mode=Any[" + ", ".join(map(lambda _: str(_.name), modes)) + "]", 558 ] 559 ) 560 raise ValueError(msg) 561 562 def _step(self) -> bool: 563 ast = self._astate # shorthand 564 565 must_stop = lambda: ast["idx"] % ast["stop_rate"] == 0 566 must_log = lambda: ast["idx"] % ast["log_rate"] == 0 567 checkpoint_enabled = ast["wb_rate"] not in (None, 0) # performing regular checkpoints? 568 must_writeback = lambda: checkpoint_enabled and (ast["idx"] % ast["wb_rate"] == 0) 569 570 def _log(msg: str = None): 571 if msg is None: # report stopping-criterion values 572 h = ast["history"][-1][0] 573 msg = [f"[{dt.datetime.now()}] Iteration {ast['idx']:>_d}"] 574 for field, value in zip(h.dtype.names, h): 575 msg.append(f"\t{field}: {value}") 576 msg = "\n".join(msg) 577 ast["logger"].info(msg) 578 579 def _update_history(): 580 def _as_struct(data: dict[str, float]) -> np.ndarray: 581 ftype = type(x) if isinstance(x := next(iter(data.values())), (int, float)) else x.dtype 582 583 spec_data = [(k, ftype) for k in data] 584 585 itype = np.int64 586 spec_iter = [("iteration", itype)] 587 588 dtype = np.dtype(spec_iter + spec_data) 589 590 utype = np.uint8 591 s = np.concatenate( # to allow mixed int/float fields: 592 [ # (1) cast to uint, then (2) to compound dtype. 593 np.array([ast["idx"]], dtype=itype).view(utype), 594 np.array(list(data.values()), dtype=ftype).view(utype), 595 ] 596 ).view(dtype) 597 return s 598 599 h = _as_struct(ast["stop_crit"].info()) 600 ast["history"].append(h) 601 602 # [Sepand] Important 603 # stop_crit.stop(), _update_history(), _log() must always be called in this order. 604 605 try: 606 _ms, _ml, _mw = must_stop(), must_log(), must_writeback() 607 608 if _ms and ast["track_objective"]: 609 self._mstate["objective_func"] = self.objective_func().reshape(-1) 610 611 if _ms and ast["stop_crit"].stop(self._mstate): 612 _update_history() 613 _log() 614 _log(msg=f"[{dt.datetime.now()}] Stopping Criterion satisfied -> END") 615 if ast["wb_rate"] is not None: 616 self.writeback() 617 return False 618 else: 619 if _ms: 620 _update_history() 621 if _ml: 622 _log() 623 if _mw: 624 self.writeback() 625 ast["idx"] += 1 626 self.m_step() 627 if _ms: 628 self._m_persist() 629 return True 630 except Exception as e: 631 msg = f"[{dt.datetime.now()}] Something went wrong -> EXCEPTION RAISED" 632 msg_xtra = f"More information: {self.logfile}." 633 print("\n".join([msg, msg_xtra]), file=sys.stderr) 634 if checkpoint_enabled: # checkpointing enabled 635 _, r = divmod(ast["idx"], ast["wb_rate"]) 636 idx_valid = ast["idx"] - r 637 msg_idx = f"Last valid checkpoint done at iteration={idx_valid}." 638 msg = "\n".join([msg, msg_idx]) 639 ast["logger"].exception(msg, exc_info=e) 640 return False 641 642 def _m_persist(self): 643 # Persist math state to avoid re-eval overhead. 644 k, v = zip(*self._mstate.items()) 645 v = pxu.compute(*v, mode="persist", traverse=False) 646 self._mstate.update(zip(k, v)) 647 # [Sepand] Note: 648 # The above evaluation strategy with `traverse=False` chosen since _mstate can hold any type 649 # of object. 650 651 def _cleanup_logger(self): 652 # Close file-handlers 653 log_name = str(self.workdir) 654 logger = logging.getLogger(log_name) 655 for handler in logger.handlers: 656 handler.close() 657
[docs] 658 def default_stop_crit(self) -> StoppingCriterion: 659 """ 660 Default stopping criterion for solver if unspecified in :py:meth:`~pyxu.abc.Solver.fit` calls. 661 662 Sub-classes are expected to overwrite this method. If not overridden, then omitting the `stop_crit` parameter 663 in :py:meth:`~pyxu.abc.Solver.fit` is forbidden. 664 """ 665 raise NotImplementedError("No default stopping criterion defined.")
666
[docs] 667 def objective_func(self) -> pxt.NDArray: 668 """ 669 Evaluate objective function given current math state. 670 671 The output array must have shape: 672 673 * (1,) if evaluated at 1 point, 674 * (N, 1) if evaluated at N different points. 675 676 Sub-classes are expected to overwrite this method. If not overridden, then enabling `track_objective` in 677 :py:meth:`~pyxu.abc.Solver.fit` is forbidden. 678 """ 679 raise NotImplementedError("No objective function defined.")
680 681 class _Worker(threading.Thread): 682 def __init__(self, solver: "Solver"): 683 super().__init__() 684 self.slvr = solver 685 686 def run(self): 687 while self.slvr.busy() and self.slvr._step(): 688 pass 689 self.slvr._astate["active"].clear()