From faaa1d1f0d671260a1a819494a522cf6917681b6 Mon Sep 17 00:00:00 2001 From: Ahmed-Bayoumy Date: Sat, 5 Oct 2024 21:34:47 -0400 Subject: [PATCH] Code linting and improvement, performance and computational improvement and modularization --- .github/workflows/win-build-and-pytest.yml | 2 +- .gitignore | 4 +- dist/OMADS-2408.1-py3-none-any.whl | Bin 106478 -> 0 bytes dist/OMADS-2410-py3-none-any.whl | Bin 0 -> 100989 bytes pyproject.toml | 1 - setup.cfg | 2 +- setup.py | 5 +- src/OMADS/Barrier.py | 84 ++- src/OMADS/Barriers.py | 706 ++++++++++----------- src/OMADS/Cache.py | 85 ++- src/OMADS/CandidatePoint.py | 110 ++-- src/OMADS/Directions.py | 172 +---- src/OMADS/Evaluator.py | 130 ++-- src/OMADS/Exploration.py | 319 ++-------- src/OMADS/Gmesh.py | 692 ++++++-------------- src/OMADS/MADS.py | 463 ++++++-------- src/OMADS/Mesh.py | 79 ++- src/OMADS/Metrics.py | 174 ++++- src/OMADS/Omesh.py | 34 +- src/OMADS/Optimizer.py | 49 +- src/OMADS/Options.py | 4 - src/OMADS/POLL.py | 130 ++-- src/OMADS/Parameters.py | 99 +-- src/OMADS/Point.py | 72 +-- src/OMADS/PostProcess.py | 48 +- src/OMADS/PreExploration.py | 78 +-- src/OMADS/PrePoll.py | 63 +- src/OMADS/SEARCH.py | 308 +++------ src/OMADS/_common.py | 35 +- src/OMADS/_globals.py | 2 - tests/OMADS_MO_BASIC.py | 391 ------------ tests/test_OMADS_BASIC.py | 395 +++++++----- tests/test_OMADS_MO_BASIC.py | 625 ++++++++++++++++++ 33 files changed, 2457 insertions(+), 2904 deletions(-) delete mode 100644 dist/OMADS-2408.1-py3-none-any.whl create mode 100644 dist/OMADS-2410-py3-none-any.whl delete mode 100644 tests/OMADS_MO_BASIC.py create mode 100644 tests/test_OMADS_MO_BASIC.py diff --git a/.github/workflows/win-build-and-pytest.yml b/.github/workflows/win-build-and-pytest.yml index 8f1ba9f..e43eb82 100644 --- a/.github/workflows/win-build-and-pytest.yml +++ b/.github/workflows/win-build-and-pytest.yml @@ -30,7 +30,7 @@ jobs: pip install flake8 pytest pytest-cov pip install wheel python setup.py sdist bdist_wheel - pip install dist/OMADS-2408.1-py3-none-any.whl + pip install dist/OMADS-2410-py3-none-any.whl - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names diff --git a/.gitignore b/.gitignore index dc04558..4f9ea96 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,6 @@ visualize.py vis.py deeplearning.mplstyle CFD -temp \ No newline at end of file +temp +tests/*.log +testing* \ No newline at end of file diff --git a/dist/OMADS-2408.1-py3-none-any.whl b/dist/OMADS-2408.1-py3-none-any.whl deleted file mode 100644 index 5d0df8faba4e9ed63f8d238613c4bf8ac8b3e2b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 106478 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

_2vB60SG}-nxF?oCM=@BTDa^pVPH>P|s=Z18 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

OsU+N#tkP4?_cnP)@l^~OCz59sh^+W=Gx@+9 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 z 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 literal 0 HcmV?d00001 diff --git a/pyproject.toml b/pyproject.toml index c044f1a..2d5ddd8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,6 @@ requires = [ build-backend = "setuptools.build_meta" [tool.pytest.ini_options] -addopts = "--cov=OMADS" testpaths = [ "tests", ] diff --git a/setup.cfg b/setup.cfg index 264ebf1..8ca0629 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = OMADS -version = 2408.1 +version = 2410 author = Ahmed H. Bayoumy author_email = ahmed.bayoumy@mail.mcgill.ca description = "Python package for DFO; an implementation of the mesh adaptive direct search (MADS)." diff --git a/setup.py b/setup.py index d27eb4d..ff8fa12 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ name="OMADS", author="Ahmed H. Bayoumy", author_email="ahmed.bayoumy@mail.mcgill.ca", - version='2408.1', + version='2410', packages=find_packages(include=['OMADS', 'OMADS.*']), description="Mesh Adaptive Direct Search (MADS)", install_requires=[ @@ -14,7 +14,8 @@ 'scipy', 'pyDOE2', 'samplersLib>=2408', - 'paramiko>=3.4.0' + 'paramiko>=3.4.0', + 'deap==1.4' ], extras_require={ 'interactive': ['matplotlib>=3.5.2', 'plotly>=5.14.1'], diff --git a/src/OMADS/Barrier.py b/src/OMADS/Barrier.py index 1e80c20..51b08e1 100644 --- a/src/OMADS/Barrier.py +++ b/src/OMADS/Barrier.py @@ -1,28 +1,27 @@ import copy from dataclasses import dataclass, field -from typing import List, Any +from typing import List, Optional from .CandidatePoint import CandidatePoint from .Point import Point -from ._globals import * +from ._globals import DType, DESIGN_STATUS, EVAL_TYPE import numpy as np from typing import Protocol -from .Parameters import Parameters from .Cache import Cache @dataclass class BarrierData(Protocol): - _xFeas: List[CandidatePoint] = None - _xInf: List[CandidatePoint] = None + _xFeas: Optional[List[CandidatePoint]] = None + _xInf: Optional[List[CandidatePoint]] = None - _xIncFeas: List[CandidatePoint] = None - _xIncInf: List[CandidatePoint] = None + _xIncFeas: Optional[List[CandidatePoint]] = None + _xIncInf: Optional[List[CandidatePoint]] = None - _refBestFeas: CandidatePoint = None - _refBestInf: CandidatePoint = None + _refBestFeas: Optional[CandidatePoint] = None + _refBestInf: Optional[CandidatePoint] = None - _dtype: DType = None + _dtype: Optional[DType] = None - def init(self, xFeas: CandidatePoint = None, evalType: EVAL_TYPE = None, barrierInitializedFromCache: bool = True): + def init(self, eval_point_list: Optional[List[Point]] = None): ... def getAllXFeas(self): @@ -91,7 +90,7 @@ def getSuccessTypeOfPoints(self): def updateWithPoints(self): ... - def findPoint(self, Point: Point, foundEvalPoint: CandidatePoint): + def findPoint(self, point: Point): ... def setN(self): @@ -112,12 +111,12 @@ def findEvalPoint(self): class BarrierBase(BarrierData): - _hMax: float = np.inf + _h_max: float = np.inf _n: int = 0 - def __init__(self, hMax: float = np.inf): - self._hMax = hMax + def __init__(self, h_max: float = np.inf): + self._h_max = h_max self._n = 0 self._dtype = DType() self._xInf = [] @@ -126,25 +125,25 @@ def __init__(self, hMax: float = np.inf): self._xIncInf = [] def setN(self): - isSet: bool = False + is_set: bool = False s: str for cp in self.getAllPoints(): - if not isSet: + if not is_set: self._n = cp._n - isSet = True + is_set = True elif cp._n != self._n: s = f"Barrier has points of size {self._n} and of size {cp._n}" raise IOError(s) - if not isSet: + if not is_set: raise IOError("Barrier could not set point size") - def checkCache(self, cache: Cache): + def checkCache(self, cache: Cache = None): if cache == None: raise IOError("Cache must be instantiated before initializing Barrier.") def checkHMax(self): - if self._hMax is None or self._hMax < self._dtype.zero: + if self._h_max is None or self._h_max < self._dtype.zero: raise IOError("Barrier: hMax must be positive.") def clearXFeas(self): @@ -155,19 +154,19 @@ def clearXInf(self): del self._xIncInf def getAllPoints(self) -> List[CandidatePoint]: - allPoints: List[CandidatePoint] = [] + all_points: List[CandidatePoint] = [] if self._xFeas is None: self._xFeas = [] for cp in self._xFeas: - allPoints.append(cp) + all_points.append(cp) if self._xInf is None: self._xInf = [] for cp in self._xInf: - allPoints.append(cp) + all_points.append(cp) - return allPoints + return all_points - def getFirstPoint(self) -> CandidatePoint: + def getFirstPoint(self) -> Optional[CandidatePoint]: if self._xIncFeas and len(self._xIncFeas) > 0: return self._xIncFeas[0] elif self._xFeas and len(self._xFeas) > 0: @@ -180,43 +179,38 @@ def getFirstPoint(self) -> CandidatePoint: return None def findEvalPoint(self, cps: List[CandidatePoint] = None, cp: CandidatePoint = None): - ind = 0 for p in cps: if p.signature == cp.signature: return True, p - ind+=1 return False, p - def findPoint(self, Point: Point, foundEvalPoint: CandidatePoint) -> bool: + def findPoint(self, point: Point) -> bool: found: bool = False - evalPointList: List[CandidatePoint] = self.getAllPoints() - for cp in evalPointList: - if cp._n != Point._n: + eval_point_list: List[CandidatePoint] = self.getAllPoints() + for cp in eval_point_list: + if cp._n != point._n: raise IOError("Error: Eval points have different dimensions") - if Point == cp.coordinates: - foundEvalPoint = copy.deepcopy(cp) + if point == cp.coordinates: found = True break return found - def checkXFeas(self, xFeas: CandidatePoint = None, evalType: EVAL_TYPE = None): - if xFeas.evaluated: - self.checkXFeasIsFeas(xFeas=xFeas, evalType=evalType) - + def checkXFeas(self, x_feas: CandidatePoint = None, eval_type: EVAL_TYPE = None): + if x_feas.evaluated: + self.checkXFeasIsFeas(x_feas=x_feas, eval_type=eval_type) def getAllXFeas(self): return self._xFeas - def checkXFeasIsFeas(self, xFeas: CandidatePoint=None, evalType: EVAL_TYPE = None): - if xFeas.evaluated and xFeas.status != DESIGN_STATUS.ERROR: - h = xFeas.h - if h is None or h!= 0.0: + def checkXFeasIsFeas(self, x_feas: CandidatePoint=None, eval_type: EVAL_TYPE = None): + if x_feas.evaluated and x_feas.status != DESIGN_STATUS.ERROR: + h = x_feas.h + if h is None or not np.isclose(h, 0.0, rtol=1e-09, atol=1e-09): raise IOError(f"Error: Barrier: xFeas' h value must be 0.0, got: {h}") - - def checkXInf(self, xInf: CandidatePoint = None, evalType: EVAL_TYPE = None): - if not xInf.evaluated: + def checkXInf(self, x_inf: CandidatePoint = None, eval_type: EVAL_TYPE = None): + if not x_inf.evaluated: raise IOError("Barrier: xInf must be evaluated before being set.") diff --git a/src/OMADS/Barriers.py b/src/OMADS/Barriers.py index f3f5a34..cf6ebde 100644 --- a/src/OMADS/Barriers.py +++ b/src/OMADS/Barriers.py @@ -23,10 +23,10 @@ import copy from dataclasses import dataclass, field -from typing import List, Any, Tuple +from typing import List, Tuple, Optional from .CandidatePoint import CandidatePoint from .Point import Point -from ._globals import * +from ._globals import DType, VAR_TYPE, BARRIER_TYPES, SUCCESS_TYPES, DESIGN_STATUS, EVAL_TYPE, COMPARE_TYPE import numpy as np from .Parameters import Parameters from .Barrier import BarrierBase @@ -34,22 +34,22 @@ @dataclass class Barrier: - _params: Parameters = None + _params: Optional[Parameters] = None _eval_type: int = 1 _h_max: float = 0 - _best_feasible: CandidatePoint = None - _ref: CandidatePoint = None - _filter: List[CandidatePoint] = None + _best_feasible: Optional[CandidatePoint] = None + _ref: Optional[CandidatePoint] = None + _filter: Optional[List[CandidatePoint]] = None _prefilter: int = 0 _rho_leaps: float = 0.1 - _prim_poll_center: CandidatePoint = None - _sec_poll_center: CandidatePoint = None + _prim_poll_center: Optional[CandidatePoint] = None + _sec_poll_center: Optional[CandidatePoint] = None _peb_changes: int = 0 _peb_filter_reset: int = 0 - _peb_lop: List[CandidatePoint] = None - _all_inserted: List[CandidatePoint] = None - _one_eval_succ: SUCCESS_TYPES = None - _success: SUCCESS_TYPES = None + _peb_lop: Optional[List[CandidatePoint]] = None + _all_inserted: Optional[List[CandidatePoint]] = None + _one_eval_succ: Optional[SUCCESS_TYPES] = None + _success: Optional[SUCCESS_TYPES] = None def __init__(self, p: Parameters, eval_type: int = 1): self._h_max = p.get_h_max_0() @@ -78,7 +78,7 @@ def insert_feasible(self, x: CandidatePoint) -> SUCCESS_TYPES: def filter_insertion(self, x:CandidatePoint) -> bool: if not x._is_EB_passed: - return + return False if self._filter is None: self._filter = [] self._filter.append(x) @@ -107,7 +107,7 @@ def filter_insertion(self, x:CandidatePoint) -> bool: def insert_infeasible(self, x: CandidatePoint): - insert: bool = self.filter_insertion(x=x) + _ = self.filter_insertion(x=x) if not self._ref: return SUCCESS_TYPES.PS @@ -150,7 +150,7 @@ def select_poll_center(self): self._prim_poll_center = best_infeasible return - last_poll_center: CandidatePoint = CandidatePoint() + last_poll_center: Optional[CandidatePoint] = None if self._params.get_barrier_type() == BARRIER_TYPES.PB: last_poll_center = self._prim_poll_center if best_infeasible.fobj[0] < (self._best_feasible.fobj[0]-self._rho_leaps): @@ -165,10 +165,9 @@ def select_poll_center(self): def set_h_max(self, h_max): self._h_max = np.round(h_max, 2) - if self._filter is not None: - if self._filter[0].h > self._h_max: - self._filter = None - return + if self._filter is not None and self._filter[0].h > self._h_max: + self._filter = None + return if self._filter is not None: it = 0 while it != len(self._filter): @@ -190,7 +189,6 @@ def insert(self, x: CandidatePoint): if self._all_inserted is None: self._all_inserted = [] self._all_inserted.append(x) - h = x.h if x.status == DESIGN_STATUS.INFEASIBLE and (not x.is_EB_passed or x.h > self._h_max): self._one_eval_succ = SUCCESS_TYPES.US return @@ -202,7 +200,9 @@ def insert(self, x: CandidatePoint): self._success = self._one_eval_succ - def insert_VNS(self): + def insert_vns(self): + """ Not required here + """ pass def update_and_reset_success(self): @@ -221,7 +221,6 @@ def update_and_reset_success(self): break if it == 0: break - # raise RuntimeError("could not find a filter point with h < h_max after a partial success") it -= 1 if self._filter is not None: self._ref = self.get_best_infeasible() @@ -233,7 +232,7 @@ def update_and_reset_success(self): if self._ref.status is DESIGN_STATUS.FEASIBLE: self.insert_feasible(self._ref) - if not (self._ref.status is DESIGN_STATUS.INFEASIBLE or self._ref.status is DESIGN_STATUS.INFEASIBLE): + if not (self._ref.status is DESIGN_STATUS.INFEASIBLE or self._ref.status is DESIGN_STATUS.FEASIBLE): self.insert(self._ref) @@ -250,14 +249,12 @@ def reset(self): self._prefilter = None self._filter = None - # self._h_max = self._params._h_max_0() self._best_feasible = None self._ref = None self._rho_leaps = 0 self._poll_center = None self._sec_poll_center = None - # if ( self._peb_changes > 0 ): # self._params.reset_PEB_changes() self._peb_changes = 0 @@ -271,15 +268,15 @@ def reset(self): @dataclass class BarrierMO(BarrierBase): """ """ - _currentIncumbentFeas: CandidatePoint = None - _currentIncumbentInf: CandidatePoint = None - _fixedVariables: CandidatePoint = None - _xFilterInf: List[CandidatePoint] = None + _currentIncumbentFeas: Optional[CandidatePoint] = None + _currentIncumbentInf: Optional[CandidatePoint] = None + _fixedVariables: Optional[CandidatePoint] = None + _xFilterInf: Optional[List[CandidatePoint]] = None _nobj: int = 0 - _bbInputsType: List[VAR_TYPE] = None + _bbInputsType: Optional[List[VAR_TYPE]] = None _incumbentSelectionParam: int = 0 - def __init__(self, param: Parameters, options: Options, evalPointList: List[CandidatePoint]= None): + def __init__(self, param: Parameters, options: Options, eval_point_list: Optional[List[CandidatePoint]]= None): super(BarrierBase, self).__init__(hMax=param.h_max) self._nobj = param.nobj @@ -294,26 +291,25 @@ def __init__(self, param: Parameters, options: Options, evalPointList: List[Cand self.checkHMax() - if evalPointList: - self.init(fixedVariables=self._fixedVariables, evalType=None,evalPointList=evalPointList) + if eval_point_list: + self.init(eval_point_list=eval_point_list) - def init(self, fixedVariables: Point = None, evalType: EVAL_TYPE = None, evalPointList: List[Point] = None): - updated: bool - updated, _, _ = self.updateWithPoints(evalPointList) + def init(self, eval_point_list: Optional[List[Point]] = None): + _, _, _ = self.updateWithPoints(eval_point_list) def checkMeshParameters(self, x: CandidatePoint = None): mesh = copy.deepcopy(x.mesh) - meshSizeCorrection: int = 0 + mesh_size_correction: int = 0 if mesh.getdeltaMeshSize().size != x._n: - meshSizeCorrection = sum(self._fixedVariables.defined) + mesh_size_correction = sum(self._fixedVariables.defined) - if (mesh.getdeltaMeshSize().size + meshSizeCorrection != x._n - or mesh.getDeltaFrameSize().size + meshSizeCorrection != x._n - or mesh.getMeshIndex().size + meshSizeCorrection != x._n): + if (mesh.getdeltaMeshSize().size + mesh_size_correction != x._n + or mesh.getDeltaFrameSize().size + mesh_size_correction != x._n + or mesh.getMeshIndex().size + mesh_size_correction != x._n): raise IOError("Error: Mesh parameters dimensions are not compatible with EvalPoint dimension.") if not mesh.getdeltaMeshSize().is_all_defined(): @@ -326,15 +322,12 @@ def checkMeshParameters(self, x: CandidatePoint = None): raise IOError("Error: some MeshIndex components of EvalPoint passed to MO Barrier ") - def updateWithPoints(self, evalPointList: List[CandidatePoint], evalType: EVAL_TYPE = None, keepAllPoints: bool = None, updateInfeasibleIncumbentAndHmax : bool = None): + def updateWithPoints(self, eval_point_list: List[CandidatePoint]=None, keep_all_points: bool = None): updated = False - updatedFeas = False - updatedInf = False + updated_feas = False + updated_inf = False - s: str - xInfTmp: CandidatePoint - - for cp in evalPointList: + for cp in eval_point_list: self.checkMeshParameters(cp) if not cp.evaluated or cp.status == DESIGN_STATUS.ERROR: @@ -343,18 +336,18 @@ def updateWithPoints(self, evalPointList: List[CandidatePoint], evalType: EVAL_T if cp.fs.size != self._nobj: raise IOError(f"Barrier update: number of objectives is equal to {self._nobj}. Trying to add this point with number of objectives {cp.fs.size}") - updatedFeas = self.updateFeasWithPoint(evalPoint=cp, evalType=evalType, keepAllPoints=keepAllPoints) or updatedFeas + updated_feas = self.updateFeasWithPoint(eval_point=cp, keep_all_points=keep_all_points) or updated_feas # // Do separate loop on evalPointList # // Second loop update the bestInfeasible. # // Use the flag oneFeasEvalFullSuccess. # // If the flag is true hmax will not change. A point improving the best infeasible should not replace it. - for cp in evalPointList: + for cp in eval_point_list: if not cp.evaluated or cp.status == DESIGN_STATUS.ERROR: continue - updatedInf = self.updateInfWithPoint(evalPoint=cp, evalType=evalType, keepAllPoints=keepAllPoints, feasHasBeenUpdated=updatedFeas) or updatedInf + updated_inf = self.updateInfWithPoint(eval_point=cp, keep_all_points=keep_all_points) or updated_inf - updated = updated or updatedFeas or updatedInf + updated = updated or updated_feas or updated_inf if updated: self.setN() @@ -362,17 +355,17 @@ def updateWithPoints(self, evalPointList: List[CandidatePoint], evalType: EVAL_T - return updated, updatedFeas, updatedInf + return updated, updated_feas, updated_inf def updateCurrentIncumbents(self): self.updateCurrentIncumbentFeas() self.updateCurrentIncumbentInf() - def setHMax(self, hMax): - oldHMax = self._hMax - self._hMax = hMax + def setHMax(self, h_max=np.inf): + old_h_max = self._h_max + self._h_max = h_max self.checkHMax() - if hMax < oldHMax: + if h_max < old_h_max: self.updateXInfAndFilterInfAfterHMaxSet() self.updateCurrentIncumbentInf() @@ -381,68 +374,64 @@ def updateXInfAndFilterInfAfterHMaxSet(self): if len(self._xInf) == 0: return - currentInd = 0 - - isInXInf = [True] * len(self._xInf) - for xInf in self._xInf: - h = xInf.h - if h > self._hMax: - isInXInf[currentInd] = False - currentInd += 1 + current_ind = 0 + + is_in_x_inf = [True] * len(self._xInf) + for x_inf in self._xInf: + h = x_inf.h + if h > self._h_max: + is_in_x_inf[current_ind] = False + current_ind += 1 - currentInd = 0 + current_ind = 0 for _ in self._xInf: - if not isInXInf[currentInd]: - self._xInf.pop(currentInd) - currentInd += 1 + if not is_in_x_inf[current_ind]: + self._xInf.pop(current_ind) + current_ind += 1 - currentInd = 0 - isInXFilterInf = [True] *len(self._xFilterInf) - - for xFilterInf in self._xFilterInf: - h = xFilterInf.h - if h >self._hMax: - isInXFilterInf[currentInd] = False - currentInd += 1 + current_ind = 0 + is_in_x_filter_inf = [True] *len(self._xFilterInf) + + for x_filter_inf in self._xFilterInf: + h = x_filter_inf.h + if h >self._h_max: + is_in_x_filter_inf[current_ind] = False + current_ind += 1 - currentInd = 0 + current_ind = 0 for _ in self._xFilterInf: - if not isInXFilterInf[currentInd]: - self._xFilterInf.pop(currentInd) - currentInd += 1 + if not is_in_x_filter_inf[current_ind]: + self._xFilterInf.pop(current_ind) + current_ind += 1 self._xFilterInf = self.non_dominated_sort(self._xFilterInf) # // And reinsert potential infeasible non dominated points into the set of infeasible # // solutions. - currentInd = 0 - isInXinf = [False] * len(self._xFilterInf) + current_ind = 0 - for evalPoint in self._xFilterInf: - if len(self._xInf) > 0 and self.findEvalPoint(self._xFilterInf, evalPoint)[1] == self._xInf[-1]: - currentIndTmp = 0 + for eval_point in self._xFilterInf: + if len(self._xInf) > 0 and self.findEvalPoint(self._xFilterInf, eval_point)[1] == self._xInf[-1]: + current_ind_tmp = 0 insert = True - for evalPointInf in self._xFilterInf: - if currentIndTmp != currentInd: - compFlag = evalPoint.__comMO__(evalPointInf, True) - if compFlag == COMPARE_TYPE.DOMINATED: + for eval_point_inf in self._xFilterInf: + if current_ind_tmp != current_ind: + comp_flag = eval_point.__comp_mo__(eval_point_inf, True) + if comp_flag == COMPARE_TYPE.DOMINATED: insert = False break - elif compFlag == COMPARE_TYPE.DOMINATING: - isInXInf[currentIndTmp] = False - currentIndTmp += 1 - isInXInf[currentInd] = insert - currentInd += 1 + elif comp_flag == COMPARE_TYPE.DOMINATING: + is_in_x_inf[current_ind_tmp] = False + current_ind_tmp += 1 + is_in_x_inf[current_ind] = insert + current_ind += 1 - for i in range(len(isInXInf)): - if isInXInf[i]: + for i in range(len(is_in_x_inf)): + if is_in_x_inf[i]: self._xInf.append(self._xFilterInf[i]) self._xInf = self.non_dominated_sort(self._xInf) - return - - def clearXFeas(self): self._xFeas.clear() @@ -454,13 +443,13 @@ def clearXInf(self): # Update the current incumbent inf. Only the infeasible one depends on XInf (not the case for the feasible one). self.updateCurrentIncumbentInf() - def computeSuccessType(self, eval1: CandidatePoint=None, eval2: CandidatePoint=None, hMax: int=np.inf): + def computeSuccessType(self, eval1: CandidatePoint=None, eval2: CandidatePoint=None, h_max: int=np.inf): """ """ success: SUCCESS_TYPES = SUCCESS_TYPES.US if eval1 is not None: if eval2 is None: h = eval1.h - if h > hMax or h == np.inf: + if h > h_max or h == np.inf: success = SUCCESS_TYPES.US else: if eval1.status == DESIGN_STATUS.FEASIBLE: @@ -475,77 +464,74 @@ def computeSuccessType(self, eval1: CandidatePoint=None, eval2: CandidatePoint=N elif eval1.status == DESIGN_STATUS.FEASIBLE and eval2.status == DESIGN_STATUS.FEASIBLE: success = SUCCESS_TYPES.US elif eval1.status != DESIGN_STATUS.FEASIBLE and eval2.status != DESIGN_STATUS.FEASIBLE: - if eval1.h <= hMax and eval1.h < eval2.h and eval1.f > eval2.f: + if eval1.h <= h_max and eval1.h < eval2.h and eval1.f > eval2.f: success = SUCCESS_TYPES.PS else: success = SUCCESS_TYPES.US return success - def defaultComputeSuccessType(self, evalPoint1: CandidatePoint, evalPoint2: CandidatePoint, hMax: float): + def defaultComputeSuccessType(self, eval_point1: CandidatePoint, eval_point2: CandidatePoint, h_max: float): success: SUCCESS_TYPES = SUCCESS_TYPES.US - if evalPoint1: - if evalPoint2: - h = evalPoint1.h - if h > hMax or h == np.inf: - # // Even if evalPoint2 is NULL, this case is still - # // not a success. - success = SUCCESS_TYPES.US - elif evalPoint1.status == DESIGN_STATUS.FEASIBLE: - success = SUCCESS_TYPES.FS - else: - success = self.defaultComputeSuccessType(evalPoint1, evalPoint2, hMax) + if eval_point1 and eval_point2: + h = eval_point1.h + if h > h_max or h == np.inf: + # // Even if evalPoint2 is NULL, this case is still + # // not a success. + success = SUCCESS_TYPES.US + elif eval_point1.status == DESIGN_STATUS.FEASIBLE: + success = SUCCESS_TYPES.FS + else: + success = self.defaultComputeSuccessType(eval_point1, eval_point2, h_max) return success - def getSuccessTypeOfPoints(self, xFeas: CandidatePoint, xInf: CandidatePoint): - successType = SUCCESS_TYPES.US - successType2 = SUCCESS_TYPES.US - newBestFeas: CandidatePoint = CandidatePoint() - newBestInf: CandidatePoint = CandidatePoint() + def getSuccessTypeOfPoints(self, x_feas: CandidatePoint = None, x_inf: CandidatePoint = None): + success_type = SUCCESS_TYPES.US + success_type2 = SUCCESS_TYPES.US if self._currentIncumbentFeas != None or self._currentIncumbentInf != None: if not self._currentIncumbentFeas: - successType = self.defaultComputeSuccessType(xFeas, self._currentIncumbentFeas, self._hMax) + success_type = self.defaultComputeSuccessType(x_feas, self._currentIncumbentFeas, self._h_max) if not self._currentIncumbentInf: - successType = self.defaultComputeSuccessType(xInf, self._currentIncumbentInf, self._hMax) - if successType2.value > successType.value: - successType = successType2 + success_type = self.defaultComputeSuccessType(x_inf, self._currentIncumbentInf, self._h_max) + if success_type2.value > success_type.value: + success_type = success_type2 - return successType + return success_type - def checkXFeasIsFeas(self, xFeas: CandidatePoint, evalType: DESIGN_STATUS): - if xFeas.evaluated and xFeas.status != DESIGN_STATUS.ERROR: - h = xFeas.h + def checkXFeasIsFeas(self, x_feas: CandidatePoint = None, eval_type: DESIGN_STATUS = None): + if x_feas.evaluated and x_feas.status != DESIGN_STATUS.ERROR: + h = x_feas.h if h != 0: raise IOError("Error: DMultiMadsBarrier: xFeas' h value must be 0.0") - if xFeas.fs.size != self._nobj: + if x_feas.fs.size != self._nobj: raise IOError("Error: DMultiMadsBarrier: xFeas' F must be of size") def getMeshMaxFrameSize(self, pt:CandidatePoint): - maxRealVal = -1.0 - maxIntegerVal = -1.0 + max_real_val = -1.0 + max_integer_val = -1.0 # Detect if mesh is sub dimension and pt are in full dimension. - meshIsInSubDimension = False + mesh_is_in_subdimension = False mesh = pt.mesh if pt.mesh._n < pt._n: - meshIsInSubDimension = True + mesh_is_in_subdimension = True shift = 0 for i in range(pt._n): # Do not use access the frame size for fixed variables. - if meshIsInSubDimension and self._fixedVariables.defined[i]: + if mesh_is_in_subdimension and self._fixedVariables.defined[i]: shift += 1 if self._bbInputsType[i] == VAR_TYPE.REAL: - maxRealVal = max(maxRealVal, mesh.getDeltaFrameSize(i-shift)) + max_real_val = max(max_real_val, mesh.getDeltaFrameSize(i-shift)) elif self._bbInputsType[i] == VAR_TYPE.INTEGER: - maxIntegerVal = max(maxIntegerVal, mesh.getDeltaFrameSize(i-shift)) - if maxRealVal > 0.0: - return maxRealVal # Some values are real: get norm inf on these values only. - elif maxIntegerVal > 0.0: - return maxIntegerVal # No real value but some integer values: get norm inf on these values only + max_integer_val = max(max_integer_val, mesh.getDeltaFrameSize(i-shift)) + if max_real_val > 0.0: + return max_real_val # Some values are real: get norm inf on these values only. + elif max_integer_val > 0.0: + return max_integer_val # No real value but some integer values: get norm inf on these values only else: return 1.0 # Only binary variables: any elements of the iterate list can be chosen @@ -559,36 +545,36 @@ def updateCurrentIncumbentFeas(self): self._currentIncumbentFeas = self._xFeas[0] return - maxFrameSizeFeasElts = -1.0 + max_frame_size_feas_elts = -1.0 # Set max frame size of all elements for xf in self._xFeas: - maxFrameSizeFeasElts = max(self.getMeshMaxFrameSize(xf), maxFrameSizeFeasElts) + max_frame_size_feas_elts = max(self.getMeshMaxFrameSize(xf), max_frame_size_feas_elts) # Select candidates - canBeFrameCenter: List[bool] = [False] * len(self._xFeas) - nbSelectedCandidates = 0 + can_be_frame_center: List[bool] = [False] * len(self._xFeas) + nb_selected_candidates = 0 # see article DMultiMads Algorithm 4. for i in range(len(self._xFeas)): - maxFrameSizeElt = self.getMeshMaxFrameSize(self._xFeas[i]) + max_frame_size_elt = self.getMeshMaxFrameSize(self._xFeas[i]) - if (10**(-float(self._incumbentSelectionParam)) * maxFrameSizeFeasElts) <= maxFrameSizeElt: - canBeFrameCenter[i] = True - nbSelectedCandidates += 1 + if (10**(-float(self._incumbentSelectionParam)) * max_frame_size_feas_elts) <= max_frame_size_elt: + can_be_frame_center[i] = True + nb_selected_candidates += 1 # Only one point in the barrier. - if (nbSelectedCandidates == 1): - for it in range(len(canBeFrameCenter)): - if canBeFrameCenter[it]: + if (nb_selected_candidates == 1): + for it in range(len(can_be_frame_center)): + if can_be_frame_center[it]: break - if it == len(canBeFrameCenter): + if it == len(can_be_frame_center): raise IOError("Error: DMultiMadsBarrier, should not reach this condition") else: - selectedInd = it - self._currentIncumbentFeas = self._xFeas[selectedInd] + selected_ind = it + self._currentIncumbentFeas = self._xFeas[selected_ind] # Only two points in the barrier. - elif ((nbSelectedCandidates == 2) and (len(self._xFeas) == 2)): + elif ((nb_selected_candidates == 2) and (len(self._xFeas) == 2)): eval1 = self._xFeas[0] eval2 = self._xFeas[1] @@ -604,9 +590,9 @@ def updateCurrentIncumbentFeas(self): else: # First case: biobjective optimization. Points are already ranked by lexicographic order. if self._nobj: - currentBestInd = 0 - maxGap = -1.0 - currentGap: float + current_best_ind = 0 + max_gap = -1.0 + current_gap: float for obj in range(self._nobj): # Get extreme values value according to one objective fmin: float = self._xFeas[0].fs[obj] @@ -617,45 +603,45 @@ def updateCurrentIncumbentFeas(self): # Intermediate points for i in range(1, self._xFeas-1): - currentGap = self._xFeas[i+1].fs[obj]-self._xFeas[i-1].fs[obj] + current_gap = self._xFeas[i+1].fs[obj]-self._xFeas[i-1].fs[obj] self._xFeas[i-1].fs[obj] - currentGap /= (fmax-fmin) - if (canBeFrameCenter[i] and currentGap >= maxGap): - maxGap = currentGap - currentBestInd = i + current_gap /= (fmax-fmin) + if (can_be_frame_center[i] and current_gap >= max_gap): + max_gap = current_gap + current_best_ind = i # Extreme points - currentGap = 2 * (self._xFeas[len(self._xFeas)-1]).fs[obj] - (self._xFeas[len(self._xFeas)-2]).fs[obj] - currentGap /= (fmax - fmin) - if canBeFrameCenter[len(self._xFeas)-1] and currentGap >= maxGap: - maxGap = currentGap - currentBestInd = len(self._xFeas)-1 + current_gap = 2 * (self._xFeas[len(self._xFeas)-1]).fs[obj] - (self._xFeas[len(self._xFeas)-2]).fs[obj] + current_gap /= (fmax - fmin) + if can_be_frame_center[len(self._xFeas)-1] and current_gap >= max_gap: + max_gap = current_gap + current_best_ind = len(self._xFeas)-1 - currentGap = 2 * (self._xFeas[1]).fs[obj] - (self._xFeas[0]).fs[obj] - currentGap /= (fmax -fmin) + current_gap = 2 * (self._xFeas[1]).fs[obj] - (self._xFeas[0]).fs[obj] + current_gap /= (fmax -fmin) - if canBeFrameCenter[0] and currentGap >= maxGap: - maxGap = currentGap - currentBestInd = 0 - self._currentIncumbentFeas = self._xFeas[currentBestInd] + if can_be_frame_center[0] and current_gap >= max_gap: + max_gap = current_gap + current_best_ind = 0 + self._currentIncumbentFeas = self._xFeas[current_best_ind] # // More than 2 objectives else: - tmpXFeasPInd: List[Tuple[CandidatePoint, int]] = [(CandidatePoint(), 0)]*len(self._xFeas) - for i in range(len(tmpXFeasPInd)): - tmpXFeasPInd[i] = (self._xFeas[i], i) - currentBestInd = 0 - maxGap = -1.0 - currentGap: float - + tmp_x_feas_p_ind: List[Tuple[CandidatePoint, int]] = [(CandidatePoint(), 0)]*len(self._xFeas) + for i in range(len(tmp_x_feas_p_ind)): + tmp_x_feas_p_ind[i] = (self._xFeas[i], i) + current_best_ind = 0 + max_gap = -1.0 + current_gap: float + for obj in range(self._nobj): # Sort elements of tmpXFeasPInd according to objective obj (in ascending order) - tmpXFeasPInd = sorted(tmpXFeasPInd, key=lambda x: x[0].fs[obj]) + tmp_x_feas_p_ind = sorted(tmp_x_feas_p_ind, key=lambda x: x[0].fs[obj]) # Get extreme values value according to one objective - fmin = tmpXFeasPInd[0][0].fs[obj] - fmax = tmpXFeasPInd[len(tmpXFeasPInd)-1][0].fs[obj] + fmin = tmp_x_feas_p_ind[0][0].fs[obj] + fmax = tmp_x_feas_p_ind[len(tmp_x_feas_p_ind)-1][0].fs[obj] # Can happen for exemple when we have several minima or for more than three objectives if fmin == fmax: @@ -663,139 +649,139 @@ def updateCurrentIncumbentFeas(self): fmax = 1. # Intermediate points - for i in range(1, len(tmpXFeasPInd)-1): - currentGap = tmpXFeasPInd[i+1][0].fs[obj]-tmpXFeasPInd[i-1][0].fs[obj] - currentGap /= (fmax - fmin) - if canBeFrameCenter[tmpXFeasPInd[i][1]] and currentGap >= maxGap: - maxGap = currentGap - currentBestInd = tmpXFeasPInd[i][1] + for i in range(1, len(tmp_x_feas_p_ind)-1): + current_gap = tmp_x_feas_p_ind[i+1][0].fs[obj]-tmp_x_feas_p_ind[i-1][0].fs[obj] + current_gap /= (fmax - fmin) + if can_be_frame_center[tmp_x_feas_p_ind[i][1]] and current_gap >= max_gap: + max_gap = current_gap + current_best_ind = tmp_x_feas_p_ind[i][1] # Extreme points - currentGap = 2*(tmpXFeasPInd[len(tmpXFeasPInd)-1][0].fs[obj]) - tmpXFeasPInd[len(tmpXFeasPInd)-2][0].fs[obj] - currentGap /= (fmax - fmin) + current_gap = 2*(tmp_x_feas_p_ind[len(tmp_x_feas_p_ind)-1][0].fs[obj]) - tmp_x_feas_p_ind[len(tmp_x_feas_p_ind)-2][0].fs[obj] + current_gap /= (fmax - fmin) - if (canBeFrameCenter[tmpXFeasPInd[len(tmpXFeasPInd)-1][1]] and currentGap > maxGap): - maxGap = currentGap - currentBestInd = tmpXFeasPInd[len(tmpXFeasPInd)-1][1] + if (can_be_frame_center[tmp_x_feas_p_ind[len(tmp_x_feas_p_ind)-1][1]] and current_gap > max_gap): + max_gap = current_gap + current_best_ind = tmp_x_feas_p_ind[len(tmp_x_feas_p_ind)-1][1] - currentGap = 2 * tmpXFeasPInd[1][0].fs[obj] - tmpXFeasPInd[0][0].fs[obj] - currentGap /= (fmax -fmin) + current_gap = 2 * tmp_x_feas_p_ind[1][0].fs[obj] - tmp_x_feas_p_ind[0][0].fs[obj] + current_gap /= (fmax -fmin) - if (canBeFrameCenter[tmpXFeasPInd[0][1]] and currentGap > maxGap): - maxGap = currentGap - currentBestInd = tmpXFeasPInd[0][1] - self._currentIncumbentFeas = self._xFeas[currentBestInd] + if (can_be_frame_center[tmp_x_feas_p_ind[0][1]] and current_gap > max_gap): + max_gap = current_gap + current_best_ind = tmp_x_feas_p_ind[0][1] + self._currentIncumbentFeas = self._xFeas[current_best_ind] def updateCurrentIncumbentInf(self): self._currentIncumbentInf = None if len(self._xFeas) > 0 and len(self._xInf) > 0: # // Get the infeasible solution with maximum dominance move below the _hMax threshold, # // according to the set of best feasible incumbent solutions. - currentInd = 0 - maxDomMove = -np.inf + current_ind = 0 + max_dom_move = -np.inf for j in range(len(self._xInf)): # // Compute dominance move # // = min \sum_{1}^m max(fi(y) - fi(x), 0) # // y \in Fk - tmpDomMove = np.inf - evalInf = self._xInf[j] - h = evalInf.h + tmp_dom_move = np.inf + eval_inf = self._xInf[j] + h = eval_inf.h - if h <= self._hMax: - for xFeas in self._xFeas: - sumVal = 0. - evalFeas = xFeas + if h <= self._h_max: + for x_feas in self._xFeas: + sum_val = 0. + eval_feas = x_feas for i in range(self._nobj): - sumVal += max(evalFeas.fs[i]-evalInf.fs[i], 0) - if tmpDomMove > sumVal: - tmpDomMove = sumVal + sum_val += max(eval_feas.fs[i]-eval_inf.fs[i], 0) + if tmp_dom_move > sum_val: + tmp_dom_move = sum_val # Get the maximum dominance move index - if maxDomMove < tmpDomMove: - maxDomMove = tmpDomMove - currentInd = j + if max_dom_move < tmp_dom_move: + max_dom_move = tmp_dom_move + current_ind = j # // In this case, all infeasible solutions are "dominated" in terms of fvalues # // by at least one element of Fk - if maxDomMove == 0.: + if np.isclose(max_dom_move, 0., rtol=1e-09, atol=1e-09): # // In this case, get the infeasible solution below the _hMax threshold which has # // minimal dominance move, when considered a maximization problem. - minDomMove = np.inf - currentInd = 0 + min_dom_move = np.inf + current_ind = 0 for j in range(len(self._xInf)): # // Compute dominance move # // = min \sum_{1}^m max(fi(x) - fi(y), 0) # // y \in Fk - tmpDomMove = np.inf - evalInf = self._xInf[j] - h = evalInf.h - if h<= self._hMax: - for xFeas in self._xFeas: - sumVal = 0. - evalFeas = xFeas + tmp_dom_move = np.inf + eval_inf = self._xInf[j] + h = eval_inf.h + if h<= self._h_max: + for x_feas in self._xFeas: + sum_val = 0. + eval_feas = x_feas # Compute \sum_{1}^m max (fi(x) - fi(y), 0) for i in range(self._nobj): - sumVal += max(evalInf.fs[i] - evalFeas.fs[i], 0.) - if tmpDomMove > sumVal: - tmpDomMove = sumVal + sum_val += max(eval_inf.fs[i] - eval_feas.fs[i], 0.) + if tmp_dom_move > sum_val: + tmp_dom_move = sum_val # Get the minimal dominance move index - if minDomMove > tmpDomMove: - minDomMove = tmpDomMove - currentInd = j - self._currentIncumbentInf = self._xInf[currentInd] + if min_dom_move > tmp_dom_move: + min_dom_move = tmp_dom_move + current_ind = j + self._currentIncumbentInf = self._xInf[current_ind] else: self._currentIncumbentInf = self.getFirstXIncInfNoXFeas() if len(self._xInf) > 0 else None def getXInfMinH(self): - indXInfMinH = 0 - hMinVal = np.inf + ind_x_inf_min_h = 0 + h_min_val = np.inf for i in range(len(self._xInf)): - eval = self._xInf[i] - h = eval.h + my_eval = self._xInf[i] + h = my_eval.h # // By definition, all elements of _xInf or _xFilterInf have a well-defined # // h value. So, no need to check. - if h max(np.abs(objv2.coordinates)): - xInf = self._xInf[0] + x_inf = self._xInf[0] else: - xInf = self._xInf[1] + x_inf = self._xInf[1] else: if self._nobj == 2: - currentBestInd = 0 - maxGap = -1. - currentGap: float + current_best_ind = 0 + max_gap = -1. + current_gap: float for obj in range(self._nobj): # Get extreme values value according to one objective @@ -823,43 +809,43 @@ def getFirstXIncInfNoXFeas(self): # Intermediate points for i in range(1, self._xInf-1): - currentGap = self._xInf[i+1].fs[obj]-self._xInf[i-1].fs[obj] + current_gap = self._xInf[i+1].fs[obj]-self._xInf[i-1].fs[obj] self._xInf[i-1].fs[obj] - currentGap /= (fmax-fmin) - if (canBeFrameCenter[i] and currentGap >= maxGap): - maxGap = currentGap - currentBestInd = i + current_gap /= (fmax-fmin) + if (can_be_frame_center[i] and current_gap >= max_gap): + max_gap = current_gap + current_best_ind = i # Extreme points - currentGap = 2 * (self._xInf[len(self._xInf)-1]).fs[obj] - (self._xInf[len(self._xInf)-2]).fs[obj] - currentGap /= (fmax - fmin) - if canBeFrameCenter[len(self._xInf)-1] and currentGap > maxGap: - maxGap = currentGap - currentBestInd = len(self._xInf)-1 + current_gap = 2 * (self._xInf[len(self._xInf)-1]).fs[obj] - (self._xInf[len(self._xInf)-2]).fs[obj] + current_gap /= (fmax - fmin) + if can_be_frame_center[len(self._xInf)-1] and current_gap > max_gap: + max_gap = current_gap + current_best_ind = len(self._xInf)-1 - currentGap = 2 * (self._xInf[1]).fs[obj] - (self._xInf[0]).fs[obj] - currentGap /= (fmax -fmin) + current_gap = 2 * (self._xInf[1]).fs[obj] - (self._xInf[0]).fs[obj] + current_gap /= (fmax -fmin) - if canBeFrameCenter[0] and currentGap > maxGap: - maxGap = currentGap - currentBestInd = 0 - xInf = self._xInf[currentBestInd] + if can_be_frame_center[0] and current_gap > max_gap: + max_gap = current_gap + current_best_ind = 0 + x_inf = self._xInf[current_best_ind] # // More than 2 objectives else: - tmpXInfPInd: List[Tuple[CandidatePoint, int]] = [(CandidatePoint(), 0)]*len(self._xInf) - for i in range(len(tmpXInfPInd)): - tmpXInfPInd[i] = (self._xInf[i], i) - currentBestInd = 0 - maxGap = -1.0 - currentGap: float + tmp_x_inf_p_ind: List[Tuple[CandidatePoint, int]] = [(CandidatePoint(), 0)]*len(self._xInf) + for i in range(len(tmp_x_inf_p_ind)): + tmp_x_inf_p_ind[i] = (self._xInf[i], i) + current_best_ind = 0 + max_gap = -1.0 + current_gap: float for obj in range(self._nobj): # Sort elements of tmpXFeasPInd according to objective obj (in ascending order) - tmpXInfPInd = sorted(tmpXInfPInd, key=lambda x: x[0].fs[obj]) + tmp_x_inf_p_ind = sorted(tmp_x_inf_p_ind, key=lambda x: x[0].fs[obj]) # Get extreme values value according to one objective - fmin = tmpXInfPInd[0][0].fs[obj] - fmax = tmpXInfPInd[len(tmpXInfPInd)-1][0].fs[obj] + fmin = tmp_x_inf_p_ind[0][0].fs[obj] + fmax = tmp_x_inf_p_ind[len(tmp_x_inf_p_ind)-1][0].fs[obj] # Can happen for exemple when we have several minima or for more than three objectives if fmin == fmax: @@ -867,39 +853,39 @@ def getFirstXIncInfNoXFeas(self): fmax = 1. # Intermediate points - for i in range(1, len(tmpXInfPInd)-1): - currentGap = tmpXInfPInd[i+1][0].fs[obj]-tmpXInfPInd[i-1][0].fs[obj] - currentGap /= (fmax - fmin) - if canBeFrameCenter[tmpXInfPInd[i][1]] and currentGap >= maxGap: - maxGap = currentGap - currentBestInd = tmpXInfPInd[i][1] + for i in range(1, len(tmp_x_inf_p_ind)-1): + current_gap = tmp_x_inf_p_ind[i+1][0].fs[obj]-tmp_x_inf_p_ind[i-1][0].fs[obj] + current_gap /= (fmax - fmin) + if can_be_frame_center[tmp_x_inf_p_ind[i][1]] and current_gap >= max_gap: + max_gap = current_gap + current_best_ind = tmp_x_inf_p_ind[i][1] # Extreme points - currentGap = 2*(tmpXInfPInd[len(tmpXInfPInd)-1][0].fs[obj]) - tmpXInfPInd[len(tmpXInfPInd)-2][0].fs[obj] - currentGap /= (fmax - fmin) + current_gap = 2*(tmp_x_inf_p_ind[len(tmp_x_inf_p_ind)-1][0].fs[obj]) - tmp_x_inf_p_ind[len(tmp_x_inf_p_ind)-2][0].fs[obj] + current_gap /= (fmax - fmin) - if (canBeFrameCenter[tmpXInfPInd[len(tmpXInfPInd)-1][1]] and currentGap > maxGap): - maxGap = currentGap - currentBestInd = tmpXInfPInd[len(tmpXInfPInd)-1][1] + if (can_be_frame_center[tmp_x_inf_p_ind[len(tmp_x_inf_p_ind)-1][1]] and current_gap > max_gap): + max_gap = current_gap + current_best_ind = tmp_x_inf_p_ind[len(tmp_x_inf_p_ind)-1][1] - currentGap = 2 * tmpXInfPInd[1][0].fs[obj] - tmpXInfPInd[0][0].fs[obj] - currentGap /= (fmax -fmin) + current_gap = 2 * tmp_x_inf_p_ind[1][0].fs[obj] - tmp_x_inf_p_ind[0][0].fs[obj] + current_gap /= (fmax -fmin) - if (canBeFrameCenter[tmpXInfPInd[0][1]] and currentGap > maxGap): - maxGap = currentGap - currentBestInd = tmpXInfPInd[0][1] - xInf = self._xInf[currentBestInd] + if (can_be_frame_center[tmp_x_inf_p_ind[0][1]] and current_gap > max_gap): + max_gap = current_gap + current_best_ind = tmp_x_inf_p_ind[0][1] + x_inf = self._xInf[current_best_ind] - return xInf + return x_inf - def updateInfWithPoint(self, evalPoint: CandidatePoint = None, evalType: EVAL_TYPE = None, keepAllPoints: bool = None, feasHasBeenUpdated: bool = False): + def updateInfWithPoint(self, eval_point: CandidatePoint = None, keep_all_points: bool = None): updated = False - if evalPoint.evaluated and evalPoint.status != DESIGN_STATUS.FEASIBLE: + if eval_point.evaluated and eval_point.status != DESIGN_STATUS.FEASIBLE: s: str - h = evalPoint.h + h = eval_point.h - if h == np.inf or (self._hMax < np.inf and h > self._hMax): + if h == np.inf or (self._h_max < np.inf and h > self._h_max): return False else: self.setHMax(h) @@ -911,40 +897,40 @@ def updateInfWithPoint(self, evalPoint: CandidatePoint = None, evalType: EVAL_TY self._xFilterInf = [] if len(self._xInf) <= 0: - self._xInf.append(evalPoint) - self._xFilterInf.append(evalPoint) + self._xInf.append(eval_point) + self._xFilterInf.append(eval_point) self._currentIncumbentInf = self._xInf[0] updated = True else: insert = True - isInXinfFilter: List[bool] = [True] * len(self._xFilterInf) - currentInd = 0 - for xFilterInf in self._xFilterInf: - compFlag = evalPoint.__comMO__(xFilterInf) - if compFlag == COMPARE_TYPE.DOMINATED: + is_in_x_inf_filter: List[bool] = [True] * len(self._xFilterInf) + current_ind = 0 + for x_filter_inf in self._xFilterInf: + comp_flag = eval_point.__comp_mo__(x_filter_inf) + if comp_flag == COMPARE_TYPE.DOMINATED: insert = False break - elif compFlag == COMPARE_TYPE.DOMINATING: + elif comp_flag == COMPARE_TYPE.DOMINATING: updated = True - isInXinfFilter[currentInd] = False - elif compFlag == COMPARE_TYPE.EQUAL: - if (not keepAllPoints): + is_in_x_inf_filter[current_ind] = False + elif comp_flag == COMPARE_TYPE.EQUAL: + if (not keep_all_points): insert = False break - if self.findEvalPoint(self._xFilterInf, evalPoint)[0]: + if self.findEvalPoint(self._xFilterInf, eval_point)[0]: insert = False else: updated = True break - currentInd += 1 + current_ind += 1 if insert: indices_to_remove = [] for i in range(len(self._xFilterInf)): - if not isInXinfFilter[i]: + if not is_in_x_inf_filter[i]: indices_to_remove.append(i) - self._xFilterInf.append(evalPoint) + self._xFilterInf.append(eval_point) for index in sorted(indices_to_remove, reverse=True): del self._xFilterInf[index] @@ -952,26 +938,26 @@ def updateInfWithPoint(self, evalPoint: CandidatePoint = None, evalType: EVAL_TY self._xFilterInf = self.non_dominated_sort(self._xFilterInf) insert = True - currentInd = 0 - isInXinf = [True * self._xInf] + current_ind = 0 + is_in_x_inf = [True * self._xInf] - for xInf in self._xInf: - compFlag = evalPoint.__comMO__(xInf, True) - if compFlag == COMPARE_TYPE.DOMINATED: + for x_inf in self._xInf: + comp_flag = eval_point.__comp_mo__(x_inf, True) + if comp_flag == COMPARE_TYPE.DOMINATED: insert = False break - elif compFlag == COMPARE_TYPE.DOMINATING or evalPoint.__comMO__(xInf): + elif comp_flag == COMPARE_TYPE.DOMINATING or eval_point.__comp_mo__(x_inf): updated = True - isInXinf[currentInd] = False - currentInd += 1 + is_in_x_inf[current_ind] = False + current_ind += 1 if insert: indices_to_remove = [] for i in range(len(self._xInf)): - if not isInXinf[i]: + if not is_in_x_inf[i]: indices_to_remove.append(i) updated = True - self._xInf.append(evalPoint) + self._xInf.append(eval_point) for index in sorted(indices_to_remove, reverse=True): del self._xInf[index] @@ -991,7 +977,7 @@ def non_dominated_sort(self, points: List[CandidatePoint] = None): for i, p in enumerate(points): for j, q in enumerate(points): - if i != j and p.__comMO__(q) == COMPARE_TYPE.DOMINATED: + if i != j and p.__comp_mo__(q) == COMPARE_TYPE.DOMINATED: dominated_count[i] += 1 if dominated_count[i] == 0: @@ -1006,55 +992,55 @@ def non_dominated_sort(self, points: List[CandidatePoint] = None): return sorted_points - def updateFeasWithPoint(self, evalPoint: CandidatePoint = None, evalType: EVAL_TYPE = None, keepAllPoints: bool = None): + def updateFeasWithPoint(self, eval_point: CandidatePoint = None, keep_all_points: bool = None): updated = False - if evalPoint.evaluated and evalPoint.status == DESIGN_STATUS.FEASIBLE: - if evalPoint.fs.size != self._nobj: - raise IOError(f"Barrier update: number of objectives is equal to {self._nobj}. Trying to add this point with number of objectives {evalPoint.fs.size}") + if eval_point.evaluated and eval_point.status == DESIGN_STATUS.FEASIBLE: + if eval_point.fs.size != self._nobj: + raise IOError(f"Barrier update: number of objectives is equal to {self._nobj}. Trying to add this point with number of objectives {eval_point.fs.size}") if self._xFeas is None: self._xFeas = [] if len(self._xFeas) == 0: - self._xFeas.append(evalPoint) + self._xFeas.append(eval_point) updated = True self._currentIncumbentFeas = self._xFeas[0] else: insert = True - keepInXFeas = [True] * len(self._xFeas) - currentInd = 0 + keep_in_x_feas = [True] * len(self._xFeas) + current_ind = 0 for xf in self._xFeas: - compFlag: COMPARE_TYPE = evalPoint.__comMO__(xf) - if compFlag == COMPARE_TYPE.DOMINATED: + comp_flag: COMPARE_TYPE = eval_point.__comp_mo__(xf) + if comp_flag == COMPARE_TYPE.DOMINATED: insert = False break - elif compFlag == COMPARE_TYPE.DOMINATING: + elif comp_flag == COMPARE_TYPE.DOMINATING: updated = True - keepInXFeas[currentInd] = False - elif compFlag == COMPARE_TYPE.EQUAL: - if not keepAllPoints: + keep_in_x_feas[current_ind] = False + elif comp_flag == COMPARE_TYPE.EQUAL: + if not keep_all_points: insert = False break - if self.findEvalPoint(self._xFeas, evalPoint)[0]: + if self.findEvalPoint(self._xFeas, eval_point)[0]: insert = False else: updated = True break - currentInd += 1 + current_ind += 1 if insert: - currentInd = 0 + current_ind = 0 for cp in self._xFeas: - if cp.__comMO__(evalPoint) == COMPARE_TYPE.DOMINATED: - self._xFeas.pop(currentInd) - currentInd += 1 + if cp.__comp_mo__(eval_point) == COMPARE_TYPE.DOMINATED: + self._xFeas.pop(current_ind) + current_ind += 1 updated = True - dir = copy.deepcopy(evalPoint.direction) - if dir is not None: - evalPoint.mesh.enlargeDeltaFrameSize(direction=dir) + my_dir = copy.deepcopy(eval_point.direction) + if my_dir is not None: + eval_point.mesh.enlargeDeltaFrameSize(direction=my_dir) - self._xFeas.append(evalPoint) + self._xFeas.append(eval_point) # Sort according to lexicographic order. self._xFeas = self.non_dominated_sort(self._xFeas) diff --git a/src/OMADS/Cache.py b/src/OMADS/Cache.py index 8309e9a..a9d625b 100644 --- a/src/OMADS/Cache.py +++ b/src/OMADS/Cache.py @@ -1,20 +1,20 @@ import copy from dataclasses import dataclass, field import operator -from typing import List, Dict, Any +from typing import List, Dict, Any, Optional import numpy as np from .CandidatePoint import CandidatePoint -from ._globals import * +from ._globals import DESIGN_STATUS @dataclass class Cache: """ In computing, a hash table (hash map) is a data structure that implements an associative array abstract data type, a structure that can map keys to values. A hash table uses a hash function to compute an index, also called a hash code, into an array of buckets or slots, from which the desired value can be found. During lookup, the key is hashed and the resulting hash indicates where the corresponding value is stored.""" - _hash_ID: List[int] = field(default_factory=list) - _best_hash_ID: List[int] = field(default_factory=list) + _hash_id: List[int] = field(default_factory=list) + _best_hash_id: List[int] = field(default_factory=list) _cache_dict: Dict[Any, Any] = field(default_factory=lambda: {}) _n_dim: int = 0 - _isPareto: bool = False - ND_points: List[CandidatePoint] = None + _is_pareto: bool = False + nd_points: Optional[List[CandidatePoint]] = None @property def cache_dict(self)->Dict: @@ -30,24 +30,24 @@ def hash_id(self)->List[int]: :rtype: List[int] """ - return self._hash_ID + return self._hash_id @hash_id.setter def hash_id(self, other: CandidatePoint): - if hash(tuple(other.coordinates)) not in self._hash_ID: - self._hash_ID.append(hash(tuple(other.coordinates))) + if hash(tuple(other.coordinates)) not in self._hash_id: + self._hash_id.append(hash(tuple(other.coordinates))) @property - def best_hash_ID(self)->List[int]: + def best_hash_id(self)->List[int]: """A getter to return the list of hash IDs :rtype: List[int] """ - return self._best_hash_ID + return self._best_hash_id - @best_hash_ID.setter - def best_hash_ID(self, id: int): - self._best_hash_ID.append(id) + @best_hash_id.setter + def best_hash_id(self, id: int): + self._best_hash_id.append(id) @property def size(self)->int: @@ -93,53 +93,48 @@ def add_to_cache(self, x: CandidatePoint): if not isinstance(x, list): hash_value: int = hash(tuple(x.coordinates)) self._cache_dict[hash_value] = x - self._hash_ID.append(hash(tuple(x.coordinates))) + self._hash_id.append(hash(tuple(x.coordinates))) else: for i in range(len(x)): hash_value: int = hash(tuple(x[i].coordinates)) self._cache_dict[hash_value] = x[i] - self._hash_ID.append(hash(tuple(x[i].coordinates))) + self._hash_id.append(hash(tuple(x[i].coordinates))) def add_to_best_cache(self, x: CandidatePoint): - if not self._isPareto: - if x.signature in self._best_hash_ID: + if not self._is_pareto: + if x.signature in self._best_hash_id: return - if len(self._best_hash_ID) <= 0 and len(self._cache_dict) >= 1: - self._best_hash_ID.append(list(self.cache_dict.keys())[0]) + if len(self._best_hash_id) <= 0 and len(self._cache_dict) >= 1: + self._best_hash_id.append(list(self.cache_dict.keys())[0]) if not isinstance(x, list): if len(self._cache_dict) > 1: - is_infeas_dom: bool = (x.status == DESIGN_STATUS.INFEASIBLE and (x.h < self._cache_dict[self._best_hash_ID[-1]].h) ) - is_feas_dom: bool = (x.status == DESIGN_STATUS.FEASIBLE and x.fobj < self._cache_dict[self._best_hash_ID[-1]].fobj) + is_infeas_dom: bool = (x.status == DESIGN_STATUS.INFEASIBLE and (x.h < self._cache_dict[self._best_hash_id[-1]].h) ) + is_feas_dom: bool = (x.status == DESIGN_STATUS.FEASIBLE and x.fobj < self._cache_dict[self._best_hash_id[-1]].fobj) else: is_infeas_dom: bool = False is_feas_dom: bool = False if is_infeas_dom or is_feas_dom: self._n_dim = len(x.coordinates) - self._best_hash_ID.append(x.signature) + self._best_hash_id.append(x.signature) else: for i in range(len(x)): - is_infeas_dom: bool = (x[i].status == DESIGN_STATUS.INFEASIBLE and (x[i].h < self._cache_dict[self._best_hash_ID[0]].h) ) - is_feas_dom: bool = (x[i].status == DESIGN_STATUS.FEASIBLE and x[i].fobj < self._cache_dict[self._best_hash_ID[0]].fobj) + is_infeas_dom: bool = (x[i].status == DESIGN_STATUS.INFEASIBLE and (x[i].h < self._cache_dict[self._best_hash_id[0]].h) ) + is_feas_dom: bool = (x[i].status == DESIGN_STATUS.FEASIBLE and x[i].fobj < self._cache_dict[self._best_hash_id[0]].fobj) if len(self._cache_dict) == 1 or is_infeas_dom or is_feas_dom: self._n_dim = len(x[i].coordinates) - self._best_hash_ID.append(self._hash_ID[-1]) + self._best_hash_id.append(self._hash_id[-1]) else: - self.ND_points = copy.deepcopy(x) - self._best_hash_ID = [] - for i in range(len(self.ND_points)): - self._best_hash_ID.append(self.ND_points[i].signature) + self.nd_points = copy.deepcopy(x) + self._best_hash_id = [] + for i in range(len(self.nd_points)): + self._best_hash_id.append(self.nd_points[i].signature) def get_best_cache_points(self, nsamples): """ Get best points """ temp = np.zeros((nsamples, self._n_dim)) index = 0 - if not self._isPareto: - - # for i in range(len(self._best_hash_ID)-1, len(self._best_hash_ID) - nsamples, -1): - # temp[index, :] = self._cache_dict[self._best_hash_ID[i]].coordinates - # index += 1 - + if not self._is_pareto: cache_temp = dict(sorted(self._cache_dict.items(), key=operator.itemgetter(1))) for k in cache_temp: @@ -149,20 +144,20 @@ def get_best_cache_points(self, nsamples): else: break else: - for k in self.ND_points: - if index < len(temp): - temp[index, :] = k.coordinates - index += 1 - else: - break + for k in self.nd_points: + # if index < len(temp): + temp[index, :] = k.coordinates + index += 1 + # else: + # break return temp def get_cache_points(self): """ Get best points """ - temp = np.zeros((len(self._hash_ID)-1, self._n_dim)) - for i in range(1, len(self._hash_ID)): - temp[i-1, :] = self._cache_dict[self._hash_ID[i]].coordinates + temp = np.zeros((len(self._hash_id)-1, self._n_dim)) + for i in range(1, len(self._hash_id)): + temp[i-1, :] = self._cache_dict[self._hash_id[i]].coordinates return temp def get_point(self, key): diff --git a/src/OMADS/CandidatePoint.py b/src/OMADS/CandidatePoint.py index c1838b8..b2fe7b7 100644 --- a/src/OMADS/CandidatePoint.py +++ b/src/OMADS/CandidatePoint.py @@ -26,7 +26,7 @@ from typing import List, Dict, Any, Optional from numpy import sum, subtract, add, maximum, power, inf import numpy as np -from ._globals import * +from ._globals import DType, BARRIER_TYPES, MPP, DESIGN_STATUS, COMPARE_TYPE from .Gmesh import Gmesh from .Point import Point @@ -67,42 +67,42 @@ class CandidatePoint: # hash signature, in the cache memory _signature: int = 0 # numpy double data type precision - _dtype: DType = None + _dtype: Optional[DType] = None # Variables type - _var_type: List[int] = None + _var_type: Optional[List[int]] = None # Discrete set - _sets: Dict = None + _sets: Optional[Dict] = None - _var_link: List[str] = None + _var_link: Optional[List[str]] = None _status: DESIGN_STATUS = DESIGN_STATUS.UNEVALUATED - _constraints_type: List[BARRIER_TYPES] = None + _constraints_type: Optional[List[BARRIER_TYPES]] = None _is_EB_passed: bool = False - _LAMBDA: List[float] = None + _LAMBDA: Optional[List[float]] = None _RHO: float = MPP.RHO.value _hmax: float = 1. _hmin: float = inf - Eval_time: float = 0. + eval_time: float = 0. source: str = "Current run" - Model: str = "Simulation" + model: str = "Simulation" - _hzero: float = None + _hzero: Optional[float] = None - _mesh: Gmesh = None + _mesh: Optional[Gmesh] = None - _direction: Point = None + _direction: Optional[Point] = None - _fs: Point = None + _fs: Optional[Point] = None - evalNo: int = 0 + eval_no: int = 0 def __post_init__(self): self._dtype = DType() @@ -147,30 +147,30 @@ def hzero(self, value: Any) -> Any: @property - def hmax(self) -> float: - if self._hmax == 0.: + def h_max(self) -> float: + if np.isclose(self._hmax, 0., rtol=1e-09, atol=1e-09): return self._dtype.zero return self._hmax - @hmax.setter - def hmax(self, value: float): + @h_max.setter + def h_max(self, value: float): self._hmax = value @property - def RHO(self) -> float: + def rho(self) -> float: return self._RHO - @RHO.setter - def RHO(self, value: float): + @rho.setter + def rho(self, value: float): self._RHO = value @property - def LAMBDA(self) -> float: + def lambda_multipliers(self) -> float: return self._LAMBDA - @LAMBDA.setter - def LAMBDA(self, value: float): + @lambda_multipliers.setter + def lambda_multipliers(self, value: float): self._LAMBDA = value @@ -305,16 +305,11 @@ def f(self): return self._f @f.setter - def f(self, val: auto): + def f(self, val: Any): if isinstance(val, list): self._f = val else: self._f = [val] - # if self.fs is None or self.fs.size <= 0: - # self.fs = Point(len(self.f)) - # self.fs.coordinates = self._f - # else: - # self.fs.coordinates = self.f @f.deleter def f(self): @@ -325,7 +320,7 @@ def fobj(self): return self._freal @fobj.setter - def fobj(self, other: auto): + def fobj(self, other: Any): if isinstance(other, list): self._freal = other else: @@ -394,8 +389,8 @@ def __eq__(self, other) -> bool: and self.f is other.f and self.h is other.h def __lt__(self, other): - return (other.h > (self.hmax if self._is_EB_passed else self._dtype.zero) > self.__dh__(other=other)) or \ - (((self.hmax if self._is_EB_passed else self._dtype.zero) >= self.h >= 0.0) and + return (other.h > (self.h_max if self._is_EB_passed else self._dtype.zero) > self.__dh__(other=other)) or \ + (((self.h_max if self._is_EB_passed else self._dtype.zero) >= self.h >= 0.0) and max(self.__df__(other=other)) < 0) def __le__(self, other): @@ -440,14 +435,14 @@ def __eval__(self, bb_output): self.c_ineq = [self.c_ineq] self.evaluated = True """ Check the multiplier matrix """ - if self.LAMBDA is None: - self.LAMBDA = [] + if self.lambda_multipliers is None: + self.lambda_multipliers = [] for _ in range(len(self.c_ineq)): - self.LAMBDA.append(MPP.LAMBDA.value) + self.lambda_multipliers.append(MPP.LAMBDA.value) else: - if len(self.c_ineq) != len(self.LAMBDA): - for _ in range(len(self.LAMBDA), len(self.c_ineq)): - self.LAMBDA.append(MPP.LAMBDA.value) + if len(self.c_ineq) != len(self.lambda_multipliers): + for _ in range(len(self.lambda_multipliers), len(self.c_ineq)): + self.lambda_multipliers.append(MPP.LAMBDA.value) """ Check and adapt the barriers matrix""" if self.constraints_type is not None: if len(self.c_ineq) != len(self.constraints_type): @@ -486,8 +481,8 @@ def __eval__(self, bb_output): if hPB > self.hzero: self.status = DESIGN_STATUS.INFEASIBLE self.h = copy.deepcopy(hPB) - if hPB < self.hmax: - self.hmax = copy.deepcopy(hPB) + if hPB < self.h_max: + self.h_max = copy.deepcopy(hPB) else: self.is_EB_passed = False self.status = DESIGN_STATUS.INFEASIBLE @@ -495,31 +490,28 @@ def __eval__(self, bb_output): self.__penalize__(extreme= True) return """ Aggregate all constraints """ - # self.h = sum(power(maximum(self.c_ineq, self._dtype.zero, - # dtype=self._dtype.dtype), 2, dtype=self._dtype.dtype)) if np.isnan(self.h) or np.any(np.isnan(self.c_ineq)): self.h = inf self.status = DESIGN_STATUS.ERROR """ Penalize relaxable constraints violation """ if any(np.isnan(self.f)) or self.h > self.hzero: - if self.h > np.round(self.hmax, 2): + if self.h > np.round(self.h_max, 2): self.__penalize__(extreme=False) self.status = DESIGN_STATUS.INFEASIBLE else: - self.hmax = copy.deepcopy(self.h) + self.h_max = copy.deepcopy(self.h) self.status = DESIGN_STATUS.FEASIBLE def __penalize__(self, extreme: bool=True): - if len(self.cPB) > len(self.LAMBDA): - self.LAMBDA += [self.LAMBDA[-1]] * abs(len(self.LAMBDA)-len(self.cPB)) - if 0 < len(self.cPB) < len(self.LAMBDA): - del self.LAMBDA[len(self.cPB):] + if len(self.cPB) > len(self.lambda_multipliers): + self.lambda_multipliers += [self.lambda_multipliers[-1]] * abs(len(self.lambda_multipliers)-len(self.cPB)) + if 0 < len(self.cPB) < len(self.lambda_multipliers): + del self.lambda_multipliers[len(self.cPB):] if extreme: - # self.f = [inf]*len(self.f) self.hmin = inf else: - self.hmin = np.dot(self.LAMBDA, self.cPB) + ((1/(2*self.RHO)) * self.h if self.RHO > 0. else np.inf) + self.hmin = np.dot(self.lambda_multipliers, self.cPB) + ((1/(2*self.rho)) * self.h if self.rho > 0. else np.inf) self.f = [self.fobj[i] * (1./len(self.fobj)) + self.hmin for i in range(len(self.fobj))] def __is_duplicate__(self, other) -> bool: @@ -537,15 +529,15 @@ def __df__(self, other): def __dh__(self, other): return subtract(self.h, other.h, dtype=self._dtype.dtype) - def __comMO__(self, other, onlyfvalues: bool = False): - compareFlag: COMPARE_TYPE = COMPARE_TYPE.UNDEFINED + def __comp_mo__(self, other, onlyfvalues: bool = False): + compare_flag: COMPARE_TYPE = COMPARE_TYPE.UNDEFINED f1 = self.fs h1 = self.h f2 = other.fs h2 = other.h if f1.size != f2.size: - return compareFlag + return compare_flag # // The comparison code has been adapted from # // Jaszkiewicz, A., & Lust, T. (2018). @@ -563,9 +555,9 @@ def __comMO__(self, other, onlyfvalues: bool = False): if isworse and isbetter: break if isworse: - compareFlag = COMPARE_TYPE.INDIFFERENT if isbetter else COMPARE_TYPE.DOMINATED + compare_flag = COMPARE_TYPE.INDIFFERENT if isbetter else COMPARE_TYPE.DOMINATED else: - compareFlag = COMPARE_TYPE.DOMINATING if isbetter else COMPARE_TYPE.EQUAL + compare_flag = COMPARE_TYPE.DOMINATING if isbetter else COMPARE_TYPE.EQUAL elif (self.status != DESIGN_STATUS.FEASIBLE and other.status != DESIGN_STATUS.FEASIBLE): if h1 != np.inf: isbetter = False @@ -583,9 +575,9 @@ def __comMO__(self, other, onlyfvalues: bool = False): if h2 < h1: isworse = True if isworse: - compareFlag = COMPARE_TYPE.INDIFFERENT if isbetter else COMPARE_TYPE.DOMINATED + compare_flag = COMPARE_TYPE.INDIFFERENT if isbetter else COMPARE_TYPE.DOMINATED else: - compareFlag = COMPARE_TYPE.DOMINATING if isbetter else COMPARE_TYPE.EQUAL + compare_flag = COMPARE_TYPE.DOMINATING if isbetter else COMPARE_TYPE.EQUAL - return compareFlag + return compare_flag diff --git a/src/OMADS/Directions.py b/src/OMADS/Directions.py index f36e0d1..15b1297 100644 --- a/src/OMADS/Directions.py +++ b/src/OMADS/Directions.py @@ -23,19 +23,13 @@ import copy -import time from .CandidatePoint import CandidatePoint from .Point import Point -# from .Barriers import Barrier, BarrierMO, BarrierBase -from ._common import logger -from dataclasses import dataclass, field -from typing import List, Dict, Any -from .Gmesh import Gmesh -from .Cache import Cache -from .Evaluator import Evaluator -from ._globals import * -from .Optimizer import GenericSamplerBase, ConstraintsRelaxationParameters - +from dataclasses import dataclass +from typing import List, Dict +from ._globals import DType, VAR_TYPE, BARRIER_TYPES, SUCCESS_TYPES, DESIGN_STATUS, MSG_TYPE +from .Optimizer import GenericSamplerBase +import numpy as np @dataclass @@ -256,7 +250,7 @@ def generate_dir(self): def ran(self): return np.random.random(self._n).astype(dtype=self._dtype.dtype) - def create_housholder(self, is_rich: bool, domain: List[int] = None, is_oneDir: bool=False) -> np.ndarray: + def create_housholder(self, is_rich: bool, domain: List[int] = None, is_one_dir: bool=False) -> np.ndarray: """Create householder matrix :param is_rich: A flag that indicates if the rich direction option is enabled @@ -301,7 +295,7 @@ def create_housholder(self, is_rich: bool, domain: List[int] = None, is_oneDir: if domain[j] != VAR_TYPE.REAL: hhm[i][j] = int(np.floor(-1 + 2**self.mesh.getdeltaMeshSize().coordinates[i])) - if is_oneDir: + if is_one_dir: return hhm else: hhm = np.vstack((hhm, -hhm)) @@ -324,7 +318,6 @@ def create_poll_set(self, hhm: np.ndarray, ub: List[float], lb: List[float], it: del self.poll_set del self.poll_dirs if is_prim: - # del self.poll_set temp = np.add(hhm, np.array(self.xmin.coordinates), dtype=self._dtype.dtype) else: temp = np.add(hhm, np.array(self.x_sc.coordinates), dtype=self._dtype.dtype) @@ -381,8 +374,6 @@ def scale(self, ub: List[float], lb: List[float], factor: float = 10.0): def directional_scaling(self, p: CandidatePoint, npts: int = 5) -> List[CandidatePoint]: lb = self.lb ub = self.ub - # np.random.seed(self.seed) - # scaling = [self.mesh.msize, 2*self.mesh.msize] scaling = self.mesh.getdeltaMeshSize().coordinates p_trials: List[CandidatePoint] = [0]*len(scaling) for k in range(len(scaling)): @@ -402,7 +393,6 @@ def gauss_perturbation(self, p: CandidatePoint, npts: int = 5) -> List[Candidate # np.random.seed(self.seed) cs = np.zeros((npts, p.n_dimensions)) pts: List[CandidatePoint] = [0] * npts - mp = 1. for k in range(p.n_dimensions): if p.var_type[k] == VAR_TYPE.REAL: cs[:, k] = np.random.normal(loc=p.coordinates[k], scale=self.mesh.getdeltaMeshSize().coordinates[k], size=(npts,)) @@ -422,141 +412,14 @@ def gauss_perturbation(self, p: CandidatePoint, npts: int = 5) -> List[Candidate return pts -# Deprecated evaluation routine - # def evaluate_candidate_point(self, index: int): - # """ Evaluate the point i on the poll set """ - # """ Set the dynamic index for this point """ - # tic = time.perf_counter() - # self.point_index = index - # if self.log is not None and self.log.isVerbose: - # self.log.log_msg(msg=f"Evaluate poll point # {index}...", msg_type=MSG_TYPE.INFO) - # """ Initialize stopping and success conditions""" - # stop: bool = False - # """ Copy the point i to a trial one """ - # xtry: CandidatePoint = self.poll_set[index] - # """ This is a success bool parameter used for - # filtering out successful designs to be printed - # in the output results file""" - # success = SUCCESS_TYPES.US - - # """ Check the cache memory; check if the trial point - # is a duplicate (it has already been evaluated) """ - # unique_p_trials: int = 0 - # is_duplicate: bool = (self.check_cache and self.hashtable.size > 0 and self.hashtable.is_duplicate(xtry)) - # while is_duplicate and unique_p_trials < 5: - # if self.display: - # print(f'Cache hit. Trial# {unique_p_trials}: Looking for a non-duplicate along the poll direction where the duplicate point is located...') - # if xtry.var_type is None: - # if self.xmin.var_type is not None: - # xtry.var_type = self.xmin.var_type - # else: - # xtry.var_type = [VAR_TYPE.CONTINUOUS] * len(self.xmin.coordinates) - # xtries: List[Point] = self.directional_scaling(p=xtry, npts=len(self.poll_dirs)*2) - # for tr in range(len(xtries)): - # is_duplicate = self.hashtable.is_duplicate(xtries[tr]) - # if is_duplicate: - # continue - # else: - # xtry = copy.deepcopy(xtries[tr]) - # break - # unique_p_trials += 1 - - # if (is_duplicate): - # if self.log is not None and self.log.isVerbose: - # self.log.log_msg(msg="Cache hit ... Failed to find a non-duplicate alternative.", msg_type=MSG_TYPE.INFO) - # if self.display: - # print("Cache hit ... Failed to find a non-duplicate alternative.") - # stop = True - # bb_eval = copy.deepcopy(self.bb_eval) - # xtry.fobj = [np.inf] * self.mesh._pbParams.nobj - # psize = copy.deepcopy(self.mesh.getDeltaFrameSize().coordinates) - # return [stop, index, self.bb_handle.bb_eval, success, psize, xtry] - - # """ Evaluation of the blackbox; get output responses """ - # if xtry.sets is not None and isinstance(xtry.sets,dict): - # p: List[Any] = [] - # for i in range(len(xtry.var_type)): - # if (xtry.var_type[i] == VAR_TYPE.DISCRETE or xtry.var_type[i] == VAR_TYPE.CATEGORICAL) and xtry.var_link[i] is not None: - # p.append(xtry.sets[xtry.var_link[i]][int(xtry.coordinates[i])]) - # else: - # p.append(xtry.coordinates[i]) - # self.bb_output, _ = self.bb_handle.eval(p) - # else: - # self.bb_output, _ = self.bb_handle.eval(xtry.coordinates) - - # """ - # Evaluate the poll point: - # - Set multipliers and penalty - # - Evaluate objective function - # - Evaluate constraint functions (can be an empty vector) - # - Aggregate constraints - # - Penalize the objective (extreme barrier) - # """ - # xtry.LAMBDA = copy.deepcopy(self.constraints_RP.LAMBDA) - # xtry.RHO = copy.deepcopy(self.constraints_RP.RHO) - # xtry.hmax = copy.deepcopy(self.constraints_RP.hmax) - # xtry.constraints_type = copy.deepcopy(self.constraints_RP.constraints_type) - # xtry.__eval__(self.bb_output) - # if not self.hashtable._isPareto: - # self.hashtable.add_to_best_cache(xtry) - # self.constraints_RP.hmax = copy.deepcopy(xtry.hmax) - # toc = time.perf_counter() - # xtry.Eval_time = (toc - tic) - - - # """ Update multipliers and penalty """ - # if self.constraints_RP.LAMBDA == None: - # self.constraints_RP.LAMBDA = self.xmin.LAMBDA - # if len(xtry.cPB) > len(self.constraints_RP.LAMBDA): - # self.constraints_RP.LAMBDA += [self.constraints_RP.LAMBDA[-1]] * abs(len(self.constraints_RP.LAMBDA)-len(xtry.cPB)) - # if len(xtry.cPB) < len(self.constraints_RP.LAMBDA): - # del self.constraints_RP.LAMBDA[len(xtry.cPB):] - # for i in range(len(xtry.cPB)): - # if self.constraints_RP.RHO == 0.: - # self.constraints_RP.RHO = 0.001 - # self.constraints_RP.LAMBDA[i] = copy.deepcopy(max(self.dtype.zero, self.constraints_RP.LAMBDA[i] + (1/self.constraints_RP.RHO)*xtry.cPB[i])) - - # if xtry.status == DESIGN_STATUS.FEASIBLE: - # self.constraints_RP.RHO *= copy.deepcopy(0.5) - - # if self.log is not None and self.log.isVerbose: - # self.log.log_msg(msg=f"Completed evaluation of point # {index} in {xtry.Eval_time} seconds, ftry={xtry.f}, status={xtry.status.name} and htry={xtry.h}. \n", msg_type=MSG_TYPE.INFO) - - # # if xtry < self.xmin: - # # self.success = True - # # success = True - - # """ Add to the cache memory """ - # if self.store_cache: - # self.hashtable.hash_id = xtry - - # # if self.save_results or self.display: - # self.bb_eval = self.bb_handle.bb_eval - # self.psize = copy.deepcopy(self.mesh.getDeltaFrameSize().coordinates) - # psize = copy.deepcopy(self.mesh.getDeltaFrameSize().coordinates) - - - - # if success == SUCCESS_TYPES.FS and self.opportunistic and self.iter > 1: - # stop = True - - # """ Check stopping criteria """ - # if self.bb_eval >= self.eval_budget: - # self.terminate = True - # stop = True - # return [stop, index, self.bb_handle.bb_eval, success, psize, xtry] - - # return [stop, index, self.bb_handle.bb_eval, success, psize, xtry] - - def postprocess_evaluated_candidates(self, x_cps: List[CandidatePoint]): - # if len(self.hashtable._best_hash_ID) <= 0: + def postprocess_evaluated_candidates(self, x_cps: List[CandidatePoint] = None): # self.hashtable._best_hash_ID.append(self.xmin.signature) for xtry in x_cps: - if self.log is not None and self.log.isVerbose: - self.log.log_msg(msg=f"Completed evaluation of point # {xtry.evalNo} in {xtry.Eval_time} seconds, ftry={xtry.f}, status={xtry.status.name} and htry={xtry.h}. \n", msg_type=MSG_TYPE.INFO) + if self.log is not None and self.log.is_verbose: + self.log.log_msg(msg=f"Completed evaluation of point # {xtry.eval_no} in {xtry.eval_time} seconds, ftry={xtry.f}, status={xtry.status.name} and htry={xtry.h}. \n", msg_type=MSG_TYPE.INFO) """ Add to the cache memory """ self.hashtable.add_to_cache(xtry) - if not self.hashtable._isPareto: + if not self.hashtable._is_pareto: self.hashtable.add_to_best_cache(xtry) if self.store_cache and xtry.signature not in self.hashtable.hash_id: self.hashtable.hash_id = xtry @@ -567,9 +430,9 @@ def postprocess_evaluated_candidates(self, x_cps: List[CandidatePoint]): def omit_duplicates(self): temp: List[CandidatePoint] = [] for xtry in self.poll_set: - is_dup = xtry.signature in self.hashtable.hash_id if not self.hashtable._isPareto else self.hashtable.is_duplicate(xtry) + is_dup = xtry.signature in self.hashtable.hash_id if not self.hashtable._is_pareto else self.hashtable.is_duplicate(xtry) is_duplicate: bool = (self.check_cache and self.hashtable.size > 0 and is_dup) - # TODO: The commented logic below needs more investigation to make sure that it doesn't hurt. + # COMPLETED: The commented logic below needs more investigation to make sure that it doesn't hurt. # while is_duplicate and unique_p_trials < 5: # if self.display: # print(f'Cache hit. Trial# {unique_p_trials}: Looking for a non-duplicate along the poll direction where the duplicate point is located...') @@ -588,7 +451,7 @@ def omit_duplicates(self): # break # unique_p_trials += 1 if (is_duplicate): - if self.log is not None and self.log.isVerbose: + if self.log is not None and self.log.is_verbose: self.log.log_msg(msg="Cache hit ... Failed to find a non-duplicate alternative.", msg_type=MSG_TYPE.INFO) if self.display: print("Cache hit ... Failed to find a non-duplicate alternative.") @@ -608,11 +471,8 @@ def master_updates(self, x: List[CandidatePoint], peval, save_all_best: bool = F """ Check success conditions """ is_infeas_dom: bool = (xtry.status == DESIGN_STATUS.INFEASIBLE and (xtry.h < self.xmin.h) ) is_feas_dom: bool = (xtry.status == DESIGN_STATUS.FEASIBLE and xtry.fobj < self.xmin.fobj) - is_infea_improving: bool = (self.xmin.status == DESIGN_STATUS.FEASIBLE and xtry.status == DESIGN_STATUS.INFEASIBLE and (xtry.fobj < self.xmin.fobj and xtry.h <= self.xmin.hmax)) - is_feas_improving: bool = (self.xmin.status == DESIGN_STATUS.INFEASIBLE and xtry.status == DESIGN_STATUS.FEASIBLE and xtry.fobj < self.xmin.fobj) - success = SUCCESS_TYPES.US - if ((is_infeas_dom or is_feas_dom)): + if (is_infeas_dom or is_feas_dom): self.success = SUCCESS_TYPES.FS self.n_successes += 1 success = SUCCESS_TYPES.FS # <- This redundant variable is important @@ -622,7 +482,7 @@ def master_updates(self, x: List[CandidatePoint], peval, save_all_best: bool = F del self._xmin self._xmin = CandidatePoint() self._xmin = copy.deepcopy(xtry) - self.constraints_RP.hmax = copy.deepcopy(xtry.hmax) + self.constraints_RP.hmax = copy.deepcopy(xtry.h_max) if self.display: if self._dtype.dtype == np.float64: print(f"Success: fmin = {self.xmin.f} (hmin = {self.xmin.h:.15})") diff --git a/src/OMADS/Evaluator.py b/src/OMADS/Evaluator.py index 93921be..2db17c0 100644 --- a/src/OMADS/Evaluator.py +++ b/src/OMADS/Evaluator.py @@ -1,10 +1,11 @@ import copy import importlib +import platform import time -from ._globals import * +from ._globals import DType, VAR_TYPE, DESIGN_STATUS, BB_EVAL_STATUS import os -from typing import List, Dict, Any, Optional, Callable -from numpy import sum, subtract, add, maximum, minimum, power, inf +from typing import List, Any, Optional, Callable +from numpy import inf import numpy as np from inspect import signature import concurrent.futures @@ -15,6 +16,7 @@ from .Options import Options from .PostProcess import PostMADS from .Point import Point +from dataclasses import dataclass @dataclass class Evaluator: @@ -30,21 +32,21 @@ class Evaluator: :param timeout: The time out of the evaluation process """ blackbox: Any = "rosenbrock" - commandOptions: Any = None + command_options: Any = None internal: Optional[str] = None path: str = "..\\tests\\Rosen" input: str = "input.inp" output: str = "output.out" - constants: List = None + constants: Optional[List] = None bb_eval: int = 0 - _dtype: DType = None + _dtype: Optional[DType] = None timeout: float = 1000000. - local_exec_jobs: List[str] = None - candidates: List[Point] = None - directions: List[Point] = None + local_exec_jobs: Optional[List[str]] = None + candidates: Optional[List[Point]] = None + directions: Optional[List[Point]] = None mesh: List[Any] = None - constraintsRelaxation: dict = None - xmin: CandidatePoint = None + constraints_relaxation: Optional[dict] = None + xmin: Optional[CandidatePoint] = None @@ -73,76 +75,67 @@ def map_variables(self, eval_set: List[CandidatePoint]): self.directions.append(xtry.direction) self.mesh.append(xtry.mesh) - def run_callable_serial_local(self, iter:int, peval: int, eval_set:List[CandidatePoint], options: Options, post: PostMADS, psize: List[float], stepName: str = None, mesh: auto = None, constraintsRelaxation: dict = None, budget:int = 1): + def run_callable_serial_local(self, iter:int, peval: int, eval_set:List[CandidatePoint], options: Options, post: PostMADS, psize: List[float], step_name: str = None, mesh: Any = None, constraints_relaxation: dict = None, budget:int = 1): xc: List[CandidatePoint] = [] self.map_variables(eval_set) - self.constraintsRelaxation = copy.deepcopy(constraintsRelaxation) + self.constraints_relaxation = copy.deepcopy(constraints_relaxation) for it in range(len(eval_set)): peval += 1 - f = self.evaluate_BB(it) + f = self.evaluate_blackbox(it) if f.status != BB_EVAL_STATUS.UNEVALUATED: xc.append(f) if mesh: xc[-1].mesh = copy.deepcopy(mesh) post.bb_eval.append(peval) - xc[-1].evalNo = peval + xc[-1].eval_no = peval post.iter.append(iter) - if stepName: - post.step_name.append(stepName) + if step_name: + post.step_name.append(step_name) post.psize.append(psize) if options.opportunistic and len(xc) > 0 and xc[-1] < self.xmin: break if peval == budget: break - # self.constraintsRelaxation["hmax"] = eval_set[-1].hmax return xc, post, peval - def evaluate_BB(self, index: int)->List[Any]: - tic = time.perf_counter() - f, errStatus = self.eval(self.candidates[index].coordinates) + def evaluate_blackbox(self, index: int)->List[Any]: + f, err_status = self.eval(self.candidates[index].coordinates) x_cp: CandidatePoint = CandidatePoint() x_cp.coordinates = copy.deepcopy(self.candidates[index].coordinates) - x_cp.LAMBDA = copy.deepcopy(self.constraintsRelaxation["LAMBDA"]) - x_cp.RHO = copy.deepcopy(self.constraintsRelaxation["RHO"]) - x_cp.hmax = copy.deepcopy(self.constraintsRelaxation["hmax"]) - x_cp.constraints_type = copy.deepcopy(self.constraintsRelaxation["constraints_type"]) + x_cp.lambda_multipliers = copy.deepcopy(self.constraints_relaxation["LAMBDA"]) + x_cp.rho = copy.deepcopy(self.constraints_relaxation["RHO"]) + x_cp.h_max = copy.deepcopy(self.constraints_relaxation["hmax"]) + x_cp.constraints_type = copy.deepcopy(self.constraints_relaxation["constraints_type"]) x_cp.direction = copy.deepcopy(self.directions[index]) x_cp.mesh = copy.deepcopy(self.mesh[index]) x_cp.__eval__(f) - if errStatus: + if err_status: x_cp.status = DESIGN_STATUS.ERROR # if x_cp.status == DESIGN_STATUS.INFEASIBLE: # self.constraintsRelaxation["hmax"] = x_cp.hmax - if self.constraintsRelaxation["LAMBDA"] == None: - self.constraintsRelaxation["LAMBDA"] = copy.deepcopy(self.xmin.LAMBDA) - if len(x_cp.cPB) > len(self.constraintsRelaxation["LAMBDA"]): - self.constraintsRelaxation["LAMBDA"] += [self.constraintsRelaxation["LAMBDA"][-1]] * abs(len(self.constraintsRelaxation["LAMBDA"])-len(x_cp.cPB)) - if len(x_cp.cPB) < len(self.constraintsRelaxation["LAMBDA"]): - del self.constraintsRelaxation["LAMBDA"][len(x_cp.cPB):] + if self.constraints_relaxation["LAMBDA"] == None: + self.constraints_relaxation["LAMBDA"] = copy.deepcopy(self.xmin.lambda_multipliers) + if len(x_cp.cPB) > len(self.constraints_relaxation["LAMBDA"]): + self.constraints_relaxation["LAMBDA"] += [self.constraints_relaxation["LAMBDA"][-1]] * abs(len(self.constraints_relaxation["LAMBDA"])-len(x_cp.cPB)) + if len(x_cp.cPB) < len(self.constraints_relaxation["LAMBDA"]): + del self.constraints_relaxation["LAMBDA"][len(x_cp.cPB):] for i in range(len(x_cp.cPB)): - if self.constraintsRelaxation["RHO"] == 0.: - self.constraintsRelaxation["RHO"] = 0.001 - self.constraintsRelaxation["LAMBDA"][i] = copy.deepcopy(max(self.dtype.zero, self.constraintsRelaxation["LAMBDA"][i] + (1/self.constraintsRelaxation["RHO"])*x_cp.cPB[i])) + if np.isclose(self.constraints_relaxation["RHO"], 0., rtol=1e-09, atol=1e-09): + self.constraints_relaxation["RHO"] = 0.001 + self.constraints_relaxation["LAMBDA"][i] = copy.deepcopy(max(self.dtype.zero, self.constraints_relaxation["LAMBDA"][i] + (1/self.constraints_relaxation["RHO"])*x_cp.cPB[i])) if x_cp.status == DESIGN_STATUS.FEASIBLE: - self.constraintsRelaxation["RHO"] *= copy.deepcopy(0.5) - - # if self.log is not None and self.log.isVerbose: - # self.log.log_msg(msg=f"Completed evaluation of point # {index} in {x_cp.Eval_time} seconds, ftry={x_cp.f}, status={x_cp.status.name} and htry={x_cp.h}. \n", msg_type=MSG_TYPE.INFO) - # toc = time.perf_counter() - # x_cp.Eval_time = (toc - tic) + self.constraints_relaxation["RHO"] *= copy.deepcopy(0.5) return x_cp - - def run_callable_parallel_local(self, iter:int, peval: int, njobs:int, eval_set:List[CandidatePoint], options: Options, post: PostMADS, psize: List[float], mesh: auto = None, stepName: str = None, eval_call: Callable = None, constraintsRelaxation: dict = None, budget:int = 1): - bb_eval = [] + def run_callable_parallel_local(self, iter:int, peval: int, eval_set:List[CandidatePoint], options: Options, post: PostMADS, psize: List[float], mesh: Any = None, step_name: str = None, constraints_relaxation: dict = None, budget:int = 1): xc: List[CandidatePoint] = [] self.map_variables(eval_set) - self.constraintsRelaxation = copy.deepcopy(constraintsRelaxation) - with concurrent.futures.ProcessPoolExecutor(options.np) as executor: - results = [executor.submit(self.evaluate_BB, it) for it in range(len(eval_set))] + self.constraints_relaxation = copy.deepcopy(constraints_relaxation) + with concurrent.futures.ProcessPoolExecutor(max_workers=options.np) as executor: + results = [executor.submit(self.evaluate_blackbox, it) for it in range(len(eval_set))] for f in concurrent.futures.as_completed(results): # if f.result()[0]: # executor.shutdown(wait=False) @@ -153,20 +146,22 @@ def run_callable_parallel_local(self, iter:int, peval: int, njobs:int, eval_set: if mesh: xc[-1].mesh = copy.deepcopy(mesh) - xc[-1].evalNo = self.bb_eval + xc[-1].eval_no = self.bb_eval self.bb_eval = peval post.bb_eval.append(peval) post.iter.append(iter) # post.poll_dirs.append(poll.poll_dirs[f.result()[1]]) - if stepName: - post.step_name.append(stepName) + if step_name: + post.step_name.append(step_name) post.psize.append(psize) if options.opportunistic and len(xc) > 0 and xc[-1] < self.xmin: break if peval == budget: break - + else: + executor.shutdown(wait=False) + return peval, xc, post, peval # Function to execute .exe file locally @@ -184,7 +179,7 @@ def execute_on_remote(self, host, username, password, exe_path): ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ssh.connect(host, username=username, password=password) - stdin, stdout, stderr = ssh.exec_command(exe_path) + _, stdout, stderr = ssh.exec_command(exe_path) output = stdout.read().decode() error = stderr.read().decode() @@ -238,26 +233,19 @@ def eval(self, values: List[float]): is_object = False try: sig = signature(self.blackbox) - except: + except Warning: is_object = True - pass if not is_object: npar = len(sig.parameters) # Get input arguments defined for the callable inputs = str(sig).replace("(", "").replace(")", "").replace(" ","").split(',') # Check if user constants list is defined and if the number of input args of the callable matches what OMADS expects if self.constants is None: - if (npar == 1 or (npar> 0 and npar <= 3 and ('*argv' in inputs))): - try: - f_eval = self.blackbox(values) - except: - evalerr = True - logging.error(f"Callable {str(self.blackbox)} evaluation returned an error at the poll point {values}") - f_eval = [inf, [inf]] - elif (npar == 2 and ('*argv' not in inputs)): + is_argv = '*argv' in inputs + if (npar == 1 or (npar> 0 and npar <= 3 and is_argv)) or (npar == 2 and is_argv): try: f_eval = self.blackbox(values) - except: + except Warning: evalerr = True logging.error(f"Callable {str(self.blackbox)} evaluation returned an error at the poll point {values}") f_eval = [inf, [inf]] @@ -267,7 +255,7 @@ def eval(self, values: List[float]): if (npar == 2 or (npar> 0 and npar <= 3 and ('*argv' in inputs))): try: f_eval = self.blackbox(values, self.constants) - except: + except Warning: evalerr = True logging.error(f"Callable {str(self.blackbox)} evaluation returned an error at the poll point {values}") else: @@ -275,7 +263,7 @@ def eval(self, values: List[float]): else: try: f_eval = self.blackbox(values) - except: + except Warning: evalerr = True logging.error(f"Callable {str(self.blackbox)} evaluation returned an error at the poll point {values}") f_eval = [[inf], [inf]] @@ -287,7 +275,7 @@ def eval(self, values: List[float]): self.write_input(values) pwd = os.getcwd() os.chdir(self.path) - isWin = platform.platform().split('-')[0] == 'Windows' + is_win = platform.platform().split('-')[0] == 'Windows' evalerr = False timouterr = False # Check if the file is executable @@ -295,14 +283,14 @@ def eval(self, values: List[float]): if not executable: raise IOError(f"The blackbox file {str(self.blackbox)} is not an executable! Please provide a valid executable file.") # Prepare the execution command based on the running machine's OS - if isWin and self.commandOptions is None: + if is_win and self.command_options is None: cmd = self.blackbox - elif isWin: - cmd = f'{self.blackbox} {self.commandOptions}' - elif self.commandOptions is None: + elif is_win: + cmd = f'{self.blackbox} {self.command_options}' + elif self.command_options is None: cmd = f'./{self.blackbox}' else: - cmd = f'./{self.blackbox} {self.commandOptions}' + cmd = f'./{self.blackbox} {self.command_options}' try: p = subprocess.run(cmd, shell=True, timeout=self.timeout) if p.returncode != 0: diff --git a/src/OMADS/Exploration.py b/src/OMADS/Exploration.py index c8c9373..6ec7d41 100644 --- a/src/OMADS/Exploration.py +++ b/src/OMADS/Exploration.py @@ -21,55 +21,53 @@ # Copyright (C) 2022 Ahmed H. Bayoumy # # ------------------------------------------------------------------------------------# +import copy from .CandidatePoint import CandidatePoint from .Point import Point from .Barriers import Barrier, BarrierMO -# from ._common import * -from .Directions import * +from .Directions import Dirs2n import samplersLib as explore import random from matplotlib import pyplot as plt -from ._globals import * +from ._globals import DType, VAR_TYPE, SUCCESS_TYPES, DESIGN_STATUS, MSG_TYPE, SAMPLING_METHOD, SEARCH_TYPE, DIST_TYPE, STOP_TYPE from .Parameters import Parameters from .Evaluator import Evaluator -from .Optimizer import GenericSamplerBase, ConstraintsRelaxationParameters +from .Optimizer import GenericSamplerBase +from typing import List, Optional, Any +from dataclasses import dataclass +import numpy as np @dataclass -class VNS_data: - fixed_vars: List[CandidatePoint] = None +class VNSData: + fixed_vars: Optional[List[CandidatePoint]] = None nb_search_pts: int = 0 stop: bool = False stop_reason: STOP_TYPE = STOP_TYPE.NO_STOP success: SUCCESS_TYPES = SUCCESS_TYPES.US count_search: bool = False - new_feas_inc: CandidatePoint = None - new_infeas_inc: CandidatePoint = None - params: Parameters = None - # true_barrier: Barrier = None - # sgte_barrier: Barrier = None - active_barrier: auto = None + new_feas_inc: Optional[CandidatePoint] = None + new_infeas_inc: Optional[CandidatePoint] = None + params: Optional[Parameters] = None + active_barrier: Any = None @dataclass -class VNS(VNS_data): +class VNS(VNSData): """ """ _k: int = 1 _k_max: int = 100 - _old_x: CandidatePoint = None - _dist: List[DIST_TYPE] = None - _ns_dist: List[int] = None + _old_x: Optional[CandidatePoint] = None + _dist: Optional[List[DIST_TYPE]] = None + _ns_dist: Optional[List[int]] = None _rho: float = 0.1 _seed: int = 0 _rho0: float = 0.1 - def __init__(self, active_barrier: auto, stop: bool=False, true_barrier: Barrier=None, sgte_barrier: Barrier=None, params=None): + def __init__(self, active_barrier: Any, stop: bool=False, params=None): self.stop = stop self.count_search = not self.stop - # self.params._opt_only_sgte = False self._dist = [DIST_TYPE.GAUSS, DIST_TYPE.GAMMA, DIST_TYPE.EXPONENTIAL, DIST_TYPE.POISSON] - # self.true_barrier = true_barrier - # self.sgte_barrier = sgte_barrier self.active_barrier = active_barrier self.params = params @@ -137,8 +135,6 @@ def draw_from_exp(self, mean: CandidatePoint) -> List[CandidatePoint]: cs[:, i] = (np.random.exponential(scale=self._rho, size=self._ns_dist[2]))+mean.coordinates[i] - # for i in range(self._ns_dist[2]): - # pts[i].coordinates = copy.deepcopy(cs[i, :]) return cs @@ -186,21 +182,18 @@ def draw_from_binomial(self, mean: CandidatePoint) -> List[CandidatePoint]: else: cs[:, i] = (np.random.binomial(n=(mean.coordinates[i]+delta)/((1/self._rho) if self._rho > 1. else self._rho), p=(1/self._rho) if self._rho > 1. else self._rho, size=(self._ns_dist[4],))-delta) - # for i in range(self._ns_dist[2]): - # pts[i].coordinates = copy.deepcopy(cs[i, :]) return cs - def generate_samples(self, x_inc: CandidatePoint=None, dist: DIST_TYPE = None)->List[float]: + def generate_samples(self, x_inc: CandidatePoint=None, dist: DIST_TYPE = None)->Optional[List[float]]: """_summary_ """ - if isinstance(self.active_barrier, BarrierMO): - if x_inc is None: + if x_inc is None: + if isinstance(self.active_barrier, BarrierMO): x_inc = self.active_barrier.getAllPoints()[0] - elif isinstance(self.active_barrier, Barrier): - if x_inc is None: + elif isinstance(self.active_barrier, Barrier): x_inc = self.active_barrier.select_poll_center() - if x_inc or not x_inc.evaluated: + if x_inc is None or not x_inc.evaluated: return None else: if dist == DIST_TYPE.GAUSS: @@ -224,10 +217,6 @@ def generate_samples(self, x_inc: CandidatePoint=None, dist: DIST_TYPE = None)-> def run(self): if self.stop: return - # Initial - # opt_only_sgte = self.params._opt_only_sgte - - # point x if isinstance(self.active_barrier, Barrier): x: CandidatePoint = self.active_barrier._best_feasible if (x is None or not x.evaluated) and self.active_barrier._filter is not None: @@ -245,11 +234,9 @@ def run(self): if (x is None or not x.evaluated): x = self._old_x - # // update _k and _old_x: if self._old_x is not None and x != self._old_x: self._rho = np.sqrt(np.sum([abs(self._old_x.coordinates[i]-x.coordinates[i])**2 for i in range(len(self._old_x.coordinates))])) - # self._rho *= 2 self._k += 1 if self._k > self._k_max: self.stop = True @@ -283,7 +270,7 @@ def run(self): samples = np.vstack((samples, p)) c += 1 elif isinstance(self.active_barrier, BarrierMO): - if self.active_barrier._currentIncumbentFeas is not None and self.active_barrier._currentIncumbentFeas.evaluated: + if self.active_barrier._currentIncumbentInf is not None and self.active_barrier._currentIncumbentInf.evaluated: for i in range(len(self._dist)): temp = self.generate_samples(x_inc= self.active_barrier._currentIncumbentInf, dist= self._dist[i]) temp = np.unique(temp, axis=0) @@ -302,6 +289,7 @@ def __post_init__(self): self._xmin = CandidatePoint() self.bb_handle = Evaluator() self._dtype = DType() + self.exploreNew = False @property def iter(self): @@ -438,7 +426,6 @@ def scale(self, ub: List[float], lb: List[float], factor: float = 10.0): for k, x in enumerate(np.isinf(self.scaling)): if x: self.scaling[k][0] = 1.0 - s_array = np.diag(self.scaling) def get_list_of_coords_from_list_of_points(self, xps: List[CandidatePoint] = None) -> np.ndarray: coords_array = np.zeros((len(xps), self.dim)) @@ -462,7 +449,7 @@ def generate_2ngrid(self, vlim: np.ndarray = None, x_incumbent: CandidatePoint = hhm = grid.create_housholder(False, domain=self.xmin.var_type) grid.lb = vlim[:, 0] grid.ub = vlim[:, 1] - grid.hmax = self.xmin.hmax + grid.hmax = self.xmin.h_max grid.create_poll_set(hhm=hhm, ub=grid.ub, @@ -490,11 +477,10 @@ def HD_grid(self, n: int =3, vlim: np.ndarray = None) -> np.ndarray: return grid_points[:n, :] - def generate_sample_points(self, nsamples: int = None, samples_in: np.ndarray = None) -> List[CandidatePoint]: + def generate_sample_points(self, nsamples: int = None, samples_in: np.ndarray = None): """ Generate the sample points """ - xlim = [] self.nvars = len(self.prob_params.baseline) - is_AS = False + is_active_sampling = False v = np.empty((self.nvars, 2)) if self.bb_handle.bb_eval + nsamples > self.eval_budget: nsamples = self.eval_budget + self.bb_handle.bb_eval @@ -523,122 +509,83 @@ def generate_sample_points(self, nsamples: int = None, samples_in: np.ndarray = self.ns = nsamples resize = False clipping = True - # self.seed += np.random.randint(0, 10000) if self.sampling_t == SAMPLING_METHOD.FULLFACTORIAL.name: sampling = explore.samplers.FullFactorial(ns=nsamples, vlim=v, w=self.weights, c=clipping) - if clipping: - resize = True + resize = True elif self.sampling_t == SAMPLING_METHOD.RS.name: sampling = explore.samplers.RS(ns=nsamples, vlim=v) sampling.options["randomness"] = self.seed elif self.sampling_t == SAMPLING_METHOD.HALTON.name: sampling = explore.samplers.halton(ns=nsamples, vlim=v, is_ham=True) - elif self.sampling_t == SAMPLING_METHOD.LH.name: + elif self.sampling_t == SAMPLING_METHOD.LH.name or (self.iter == 1 and self.prob_params.lhs_search_initialization): sampling = explore.samplers.LHS(ns=nsamples, vlim=v) - sampling.options["randomness"] = self.seed - sampling.options["criterion"] = self.sampling_criter + sampling.options["randomness"] = self.seed+self.iter + sampling.options["criterion"] = self.sampling_criter if self.iter > 1 else "corr" sampling.options["msize"] = self.mesh.getdeltaMeshSize().coordinates is_lhs = True else: - if self.iter == 1 or (len(self.hashtable._cache_dict) if isinstance(self.activeBarrier, Barrier) or self.activeBarrier is None else len(self.hashtable._best_hash_ID)) < nsamples:# or self.n_successes / (self.iter) <= 0.25: - sampling = explore.samplers.halton(ns=nsamples, vlim=v) if isinstance(self.activeBarrier, Barrier) or self.activeBarrier is None else explore.samplers.LHS(ns=nsamples, vlim=v) + if self.exploreNew or self.iter == 1 or (len(self.hashtable._cache_dict) if isinstance(self.activeBarrier, Barrier) or self.activeBarrier is None else len(self.hashtable._best_hash_id)) < nsamples:# or self.n_successes / (self.iter) <= 0.25: + sampling = explore.samplers.halton(ns=nsamples, vlim=v) if (isinstance(self.activeBarrier, Barrier) or self.activeBarrier is None) and not self.exploreNew and self.iter == 1 else explore.samplers.LHS(ns=nsamples, vlim=v) sampling.options["randomness"] = self.seed + self.iter sampling.options["criterion"] = self.sampling_criter sampling.options["msize"] = self.mesh.getdeltaMeshSize().coordinates sampling.options["varLimits"] = v + self.exploreNew = False else: - # if len(self.hashtable._best_hash_ID) > self.best_samples: - # if len(self.hashtable._best_hash_ID) > self.best_samples: - self.best_samples = len(self.hashtable._best_hash_ID) + if self.hashtable._is_pareto: + nsamples = len(self.hashtable.nd_points) + self.best_samples = len(self.hashtable._best_hash_id) self.AS = explore.samplers.activeSampling(data=self.hashtable.get_best_cache_points(nsamples=nsamples), n_r=nsamples, vlim=v, kernel_type="Gaussian" if self.dim <= 30 else "Silverman", bw_method="SILVERMAN", seed=int(self.seed + self.iter)) - # estGrid = explore.FullFactorial(ns=nsamples, vlim=v, w=self.weights, c=clipping) - if self.estGrid is None: - if self.dim <= 30: + if self.estGrid is None and self.dim <= 30: self.estGrid = explore.samplers.FullFactorial(ns=nsamples, vlim=v, w=self.weights, c=clipping) - # else: - # if (self.iter % 2) == 0: - # self.estGrid = explore.samplers.halton(ns=nsamples, vlim=v) - # else: - # self.estGrid = explore.samplers.RS(ns=nsamples, vlim=v) - # self.estGrid.set_options(c=self.sampling_criter, r= self.seed + self.iter) - # self.estGrid.options["msize"] = self.mesh.msize - - # self.estGrid.set_options(c=self.sampling_criter, r=int(self.seed + self.iter)) self.AS.kernel.bw_method = "SILVERMAN" if self.dim <=30: S = self.estGrid.generate_samples() else: - if True: #(self.iter % 2) == 0: - if self.estGrid == None: - self.estGrid = explore.samplers.LHS(ns=nsamples, vlim=v) - S = self.estGrid.generate_samples() - else: - S = self.estGrid.expand_lhs(x=self.hashtable.get_best_cache_points(nsamples=nsamples), n_points=nsamples, method="ExactSE") + # if True: #(self.iter % 2) == 0: + if self.estGrid == None: + self.estGrid = explore.samplers.LHS(ns=nsamples, vlim=v) + S = self.estGrid.generate_samples() else: - S = self.HD_grid(n=nsamples, vlim=v) + S = self.estGrid.expand_lhs(x=self.hashtable.get_best_cache_points(nsamples=nsamples), n_points=nsamples, method="ExactSE") + # else: + # S = self.HD_grid(n=nsamples, vlim=v) if nsamples < len(S): self.AS.kernel.estimate_pdf(S[:nsamples, :]) else: self.AS.kernel.estimate_pdf(S) - is_AS = True + is_active_sampling = True if self.iter > 1 and is_lhs: - Ps = copy.deepcopy(sampling.expand_lhs(x=self.map_samples_from_points_to_coords(), n_points=nsamples, method= "basic")) + ps = copy.deepcopy(sampling.expand_lhs(x=self.map_samples_from_points_to_coords(), n_points=nsamples, method= "basic")) else: - if is_AS: - Ps = copy.deepcopy(self.AS.resample(size=nsamples, seed=int(self.seed + self.iter))) + if is_active_sampling: + ps = copy.deepcopy(self.AS.resample(size=nsamples, seed=int(self.seed + self.iter))) else: - Ps= copy.deepcopy(sampling.generate_samples()) + ps= copy.deepcopy(sampling.generate_samples()) - if False: - self.df = pd.DataFrame(Ps, columns=[f'x{i}' for i in range(self.dim)]) - pd.plotting.scatter_matrix(self.df, alpha=0.2) - plt.show() if resize: - self.ns = len(Ps) - nsamples = len(Ps) + self.ns = len(ps) + nsamples = len(ps) - # if self.xmin is not None: - # self.visualize_samples(self.xmin.coordinates[0], self.xmin.coordinates[1]) if self.iter > 1 and is_lhs: - self.map_samples_from_coords_to_points(Ps[len(Ps)-nsamples:]) + self.map_samples_from_coords_to_points(ps[len(ps)-nsamples:]) else: - self.map_samples_from_coords_to_points(Ps) - return v, Ps + self.map_samples_from_coords_to_points(ps) def project_coords_to_mesh(self, x:List[float], ref: List[float] = None): pref = Point(self.mesh._n) pref.coordinates = ref px = Point(self.mesh._n) px.coordinates = x - xProjected: Point = self.mesh.projectOnMesh(px, pref) - # if ref == None: - # ref = [0.]*len(x) - # if self.xmin.var_type is None: - # self.xmin.var_type = [VAR_TYPE.REAL] * len(self.xmin.coordinates) - # for i in range(len(x)): - # if self.xmin.var_type[i] != VAR_TYPE.CATEGORICAL: - # if self.xmin.var_type[i] == VAR_TYPE.REAL: - # x[i] = ref[i] + (np.round((x[i]-ref[i])/self.mesh.msize) * self.mesh.msize) - # else: - # x[i] = int(ref[i] + int(int((x[i]-ref[i])/self.mesh.msize) * self.mesh.msize)) - # else: - # x[i] = int(x[i]) - # if x[i] < self.prob_params.lb[i]: - # x[i] = self.prob_params.lb[i] + (self.prob_params.lb[i] - x[i]) - # if x[i] > self.prob_params.ub[i]: - # x[i] = self.prob_params.ub[i] - # if x[i] > self.prob_params.ub[i]: - # x[i] = self.prob_params.ub[i] - (x[i] - self.prob_params.ub[i]) - # if x[i] < self.prob_params.lb[i]: - # x[i] = self.prob_params.lb[i] - - return xProjected.coordinates + x_projected: Point = self.mesh.projectOnMesh(px, pref) + + return x_projected.coordinates def map_samples_from_coords_to_points(self, samples: np.ndarray): for i in range(len(samples)): @@ -668,7 +615,6 @@ def gauss_perturbation(self, p: CandidatePoint, npts: int = 5) -> List[Candidate # np.random.seed(self.seed) cs = np.zeros((npts, p.n_dimensions)) pts: List[CandidatePoint] = [0] * npts - mp = 1. for k in range(p.n_dimensions): if p.var_type[k] == VAR_TYPE.REAL: cs[:, k] = np.random.normal(loc=p.coordinates[k], scale=self.mesh.getdeltaMeshSize().coordinates[k], size=(npts,)) @@ -691,9 +637,9 @@ def gauss_perturbation(self, p: CandidatePoint, npts: int = 5) -> List[Candidate def omit_duplicates(self): temp: List[CandidatePoint] = [] for xtry in self._candidate_points_set: - is_dup = xtry.signature in self.hashtable.hash_id if not self.hashtable._isPareto else self.hashtable.is_duplicate(xtry) + is_dup = xtry.signature in self.hashtable.hash_id if not self.hashtable._is_pareto else self.hashtable.is_duplicate(xtry) is_duplicate: bool = (self.check_cache and self.hashtable.size > 0 and is_dup) - # TODO: The commented logic below needs more investigation to make sure that it doesn't hurt. + # COMPLETED: The commented logic below needs more investigation to make sure that it doesn't hurt. # while is_duplicate and unique_p_trials < 5: # if self.display: # print(f'Cache hit. Trial# {unique_p_trials}: Looking for a non-duplicate along the poll direction where the duplicate point is located...') @@ -712,7 +658,7 @@ def omit_duplicates(self): # break # unique_p_trials += 1 if (is_duplicate): - if self.log is not None and self.log.isVerbose: + if self.log is not None and self.log.is_verbose: self.log.log_msg(msg="Cache hit ... Failed to find a non-duplicate alternative.", msg_type=MSG_TYPE.INFO) if self.display: print("Cache hit ... Failed to find a non-duplicate alternative.") @@ -723,139 +669,15 @@ def omit_duplicates(self): self._candidate_points_set = [] for t in temp: self._candidate_points_set.append(copy.deepcopy(t)) - - # Deprecated routine - # def evaluate_candidate_point(self, index: int): - # """ Evaluate the sample point i on the points set """ - # """ Set the dynamic index for this point """ - # tic = time.perf_counter() - # self.point_index = index - # if self.log is not None and self.log.isVerbose: - # self.log.log_msg(msg=f"Evaluate sample point # {index}...", msg_type=MSG_TYPE.INFO) - # """ Initialize stopping and success conditions""" - # stop: bool = False - # """ Copy the point i to a trial one """ - # xtry: CandidatePoint = self._candidate_points_set[index] - # """ This is a success bool parameter used for - # filtering out successful designs to be printed - # in the output results file""" - # success = SUCCESS_TYPES.US - - # """ Check the cache memory; check if the trial point - # is a duplicate (it has already been evaluated) """ - # unique_p_trials: int = 0 - # is_duplicate: bool = (self.check_cache and self.hashtable.size > 0 and self.hashtable.is_duplicate(xtry)) - # while is_duplicate and unique_p_trials < 5: - # self.log.log_msg(f'Cache hit. Trial# {unique_p_trials}: Looking for a non-duplicate in the vicinity of the duplicate point ...', MSG_TYPE.INFO) - # if self.display: - # print(f'Cache hit. Trial# {unique_p_trials}: Looking for a non-duplicate in the vicinity of the duplicate point ...') - # if xtry.var_type is None: - # if self.xmin.var_type is not None: - # xtry.var_type = self.xmin.var_type - # else: - # xtry.var_type = [VAR_TYPE.CONTINUOUS] * len(self.xmin.coordinates) - - # xtries: List[Point] = self.gauss_perturbation(p=xtry, npts=len(self.samples)*2) - # for tr in range(len(xtries)): - # is_duplicate = self.hashtable.is_duplicate(xtries[tr]) - # if is_duplicate: - # continue - # else: - # xtry = copy.deepcopy(xtries[tr]) - # break - # unique_p_trials += 1 - - # if (is_duplicate): - # if self.log is not None and self.log.isVerbose: - # self.log.log_msg(msg="Cache hit ... Failed to find a non-duplicate alternative.", msg_type=MSG_TYPE.INFO) - # if self.display: - # print("Cache hit ... Failed to find a non-duplicate alternative.") - # stop = True - # bb_eval = copy.deepcopy(self.bb_eval) - # psize = copy.deepcopy(self.mesh.getDeltaFrameSize().coordinates) - # return [stop, index, self.bb_handle.bb_eval, success, psize, xtry] - - - # """ Evaluation of the blackbox; get output responses """ - # if xtry.sets is not None and isinstance(xtry.sets,dict): - # p: List[Any] = [] - # for i in range(len(xtry.var_type)): - # if (xtry.var_type[i] == VAR_TYPE.DISCRETE or xtry.var_type[i] == VAR_TYPE.CATEGORICAL) and xtry.var_link[i] is not None: - # p.append(xtry.sets[xtry.var_link[i]][int(xtry.coordinates[i])]) - # else: - # p.append(xtry.coordinates[i]) - # self.bb_output, _ = self.bb_handle.eval(p) - # else: - # self.bb_output, _ = self.bb_handle.eval(xtry.coordinates) - - # """ - # Evaluate the poll point: - # - Set multipliers and penalty - # - Evaluate objective function - # - Evaluate constraint functions (can be an empty vector) - # - Aggregate constraints - # - Penalize the objective (extreme barrier) - # """ - # xtry.constraints_type = copy.deepcopy(self.constraints_RP.constraints_type) - # xtry.LAMBDA = copy.deepcopy(self.constraints_RP.LAMBDA) - # xtry.RHO = copy.deepcopy(self.constraints_RP.RHO) - # xtry.hmax = copy.deepcopy(self.constraints_RP.hmax) - # xtry.constraints_type = copy.deepcopy(self.prob_params.constraints_type) - # # xtry.__eval__(self.bb_output) - # # if not self.hashtable._isPareto: - # # self.hashtable.add_to_best_cache(xtry) - # # toc = time.perf_counter() - # # xtry.Eval_time = (toc - tic) - - # """ Update multipliers and penalty """ - # if self.constraints_RP.LAMBDA == None: - # self.constraints_RP.LAMBDA = self.xmin.LAMBDA - # if len(xtry.c_ineq) > len(self.constraints_RP.LAMBDA): - # self.constraints_RP.LAMBDA += [self.constraints_RP.LAMBDA[-1]] * abs(len(self.constraints_RP.LAMBDA)-len(xtry.c_ineq)) - # if len(xtry.c_ineq) < len(self.constraints_RP.LAMBDA): - # del self.constraints_RP.LAMBDA[len(xtry.c_ineq):] - # for i in range(len(xtry.c_ineq)): - # if self.constraints_RP.RHO == 0.: - # self.constraints_RP.RHO = 0.001 - # if self.constraints_RP.LAMBDA is None: - # self.constraints_RP.LAMBDA = xtry.LAMBDA - # self.constraints_RP.LAMBDA[i] = copy.deepcopy(max(self.dtype.zero, self.constraints_RP.LAMBDA[i] + (1/self.constraints_RP.RHO)*xtry.c_ineq[i])) - - # if xtry.status == DESIGN_STATUS.FEASIBLE: - # self.constraints_RP.RHO *= copy.deepcopy(0.5) - - # if self.log is not None and self.log.isVerbose: - # self.log.log_msg(msg=f"Completed evaluation of point # {index} in {xtry.Eval_time} seconds, ftry={xtry.f}, status={xtry.status.name} and htry={xtry.h}. \n", msg_type=MSG_TYPE.INFO) - - # """ Add to the cache memory """ - # if self.store_cache: - # self.hashtable.hash_id = xtry - - # # if self.save_results or self.display: - # self.bb_eval = self.bb_handle.bb_eval - # self.psize = copy.deepcopy(self.mesh.getDeltaFrameSize().coordinates) - # psize = copy.deepcopy(self.mesh.getDeltaFrameSize().coordinates) - - # if xtry < self.xmin: - # success = SUCCESS_TYPES.FS - - # if success == SUCCESS_TYPES.FS and self.opportunistic and self.iter > 1: - # stop = True - - # """ Check stopping criteria """ - # if self.bb_eval >= self.eval_budget: - # self.terminate = True - # stop = True - # return [stop, index, self.bb_handle.bb_eval, success, psize, xtry] - def postprocess_evaluated_candidates(self, x_cps: List[CandidatePoint]): + def postprocess_evaluated_candidates(self, x_cps: List[CandidatePoint] = None): for xtry in x_cps: - if self.log is not None and self.log.isVerbose: - self.log.log_msg(msg=f"Completed evaluation of point # {xtry.evalNo} in {xtry.Eval_time} seconds, ftry={xtry.f}, status={xtry.status.name} and htry={xtry.h}. \n", msg_type=MSG_TYPE.INFO) + if self.log is not None and self.log.is_verbose: + self.log.log_msg(msg=f"Completed evaluation of point # {xtry.eval_no} in {xtry.eval_time} seconds, ftry={xtry.f}, status={xtry.status.name} and htry={xtry.h}. \n", msg_type=MSG_TYPE.INFO) """ Add to the cache memory """ self.hashtable.add_to_cache(xtry) - if not self.hashtable._isPareto: + if not self.hashtable._is_pareto: self.hashtable.add_to_best_cache(xtry) if self.store_cache and xtry.signature not in self.hashtable.hash_id: self.hashtable.hash_id = xtry @@ -863,7 +685,6 @@ def postprocess_evaluated_candidates(self, x_cps: List[CandidatePoint]): # if self.save_results or self.display: self.bb_eval = self.bb_handle.bb_eval self.psize = copy.deepcopy(self.mesh.getDeltaFrameSize().coordinates) - psize = copy.deepcopy(self.mesh.getDeltaFrameSize().coordinates) def master_updates(self, x: List[CandidatePoint], peval, save_all_best: bool = False, save_all:bool = False): if peval >= self.eval_budget: @@ -875,11 +696,9 @@ def master_updates(self, x: List[CandidatePoint], peval, save_all_best: bool = F """ Check success conditions """ is_infeas_dom: bool = (xtry.status == DESIGN_STATUS.INFEASIBLE and (xtry.h < self.xmin.h) ) is_feas_dom: bool = (xtry.status == DESIGN_STATUS.FEASIBLE and xtry.fobj < self.xmin.fobj) - is_infea_improving: bool = (self.xmin.status == DESIGN_STATUS.FEASIBLE and xtry.status == DESIGN_STATUS.INFEASIBLE and (xtry.fobj < self.xmin.fobj and xtry.h <= self.xmin.hmax)) - is_feas_improving: bool = (self.xmin.status == DESIGN_STATUS.INFEASIBLE and xtry.status == DESIGN_STATUS.FEASIBLE and xtry.fobj < self.xmin.fobj) success = SUCCESS_TYPES.US - if ((is_infeas_dom or is_feas_dom)): + if (is_infeas_dom or is_feas_dom): self.success = SUCCESS_TYPES.FS success = SUCCESS_TYPES.US # <- This redundant variable is important # for managing concurrent parallel execution @@ -907,6 +726,8 @@ def master_updates(self, x: List[CandidatePoint], peval, save_all_best: bool = F return x_post def update_local_region(self, region="expand"): + if self.vicinity_ratio is None: + self.vicinity_ratio = np.ones((len(self.prob_params.baseline),1)) if region =="expand": for i in range(len(self.vicinity_ratio)): if self.vicinity_ratio[i] * 2 < self.prob_params.ub[i]: @@ -923,6 +744,6 @@ class search_sampling: s_method: str = SAMPLING_METHOD.LH.name ns: int = 3 visualize: bool = False - criterion: str = None - weights: List[float] = None + criterion: Optional[str] = None + weights: Optional[List[float]] = None type: str = SEARCH_TYPE.SAMPLING.name \ No newline at end of file diff --git a/src/OMADS/Gmesh.py b/src/OMADS/Gmesh.py index 2d82b9e..0d78364 100644 --- a/src/OMADS/Gmesh.py +++ b/src/OMADS/Gmesh.py @@ -20,56 +20,54 @@ # https://github.com/Ahmed-Bayoumy/OMADS # # Copyright (C) 2022 Ahmed H. Bayoumy # # ------------------------------------------------------------------------------------# +from dataclasses import dataclass -import copy -from typing import List -from ._globals import * +import numpy as np +from ._globals import DType, GL_LIMITS from .Point import Point from .Mesh import Mesh from .Options import Options from .Parameters import Parameters +from typing import Any, Optional @dataclass class Gmesh(Mesh): """ GMesh: Granular mesh """ - _initFrameSizeExp: Point = None - _frameSizeMant: Point = None - _frameSizeExp: Point = None - _finestMeshSize: Point = None - _granularity: Point = None - _enforceSanityChecks: bool = None - _allGranular: bool = None - _anisotropyFactor: float = None - _anisotropicMesh: bool = None + _initFrameSizeExp: Optional[Point] = None + _frameSizeMant: Optional[Point] = None + _frameSizeExp: Optional[Point] = None + _finestMeshSize: Optional[Point] = None + _granularity: Optional[Point] = None + _enforceSanityChecks: Optional[bool] = None + _allGranular: Optional[bool] = None + _anisotropyFactor: Optional[float] = None + _anisotropicMesh: Optional[bool] = None _refineFreq: int = 1 - _refineCount: int = None - _r: Point = None - _r_min: Point = None - _r_max: Point = None - _Delta_0: Point = None - _Delta_0_mant: Point = None - _pos_mant_0: Point = None + _refineCount: Optional[int] = None + _r: Optional[Point] = None + _r_min: Optional[Point] = None + _r_max: Optional[Point] = None + _Delta_0: Optional[Point] = None + _Delta_0_mant: Optional[Point] = None + _pos_mant_0: Optional[Point] = None _HARD_MIN_MESH_INDEX: int = -300 - def __init__(self, pbParam: Parameters, runOptions: Options): + def __init__(self, pb_param: Parameters, run_options: Options): """ Constructor """ - super(Gmesh, self).__init__(pbParams=pbParam, limitMaxMeshIndex=-GL_LIMITS, limitMinMeshIndex=GL_LIMITS) - - # if (self._limit_mesh_index>0): - # raise IOError("Limit mesh index must be <=0 ") - + super(Gmesh, self).__init__(pb_params=pb_param, limit_max_mesh_index=-GL_LIMITS, limit_min_mesh_index=GL_LIMITS) + self._initFrameSizeExp = Point() self._frameSizeMant = Point() self._frameSizeExp = Point() self._finestMeshSize = Point() - self._granularity = pbParam.granularity + self._granularity = pb_param.granularity self._enforceSanityChecks = True self._allGranular = True - self._anisotropyFactor = runOptions.anisotropyFactor - self._anisotropicMesh = runOptions.anistropicMesh - self._refineFreq = runOptions.refineFreq + self._anisotropyFactor = run_options.anisotropyFactor + self._anisotropicMesh = run_options.anistropicMesh + self._refineFreq = run_options.refineFreq self._refineCount = 0 - self._dtype = DType(runOptions.precision) + self._dtype = DType(run_options.precision) self.init() @property @@ -80,64 +78,64 @@ def dtype(self): def dtype(self, other: DType): self.dtype = other - def initFrameSizeGranular(self, initialFrameSize: Point): - if not initialFrameSize.is_all_defined() or initialFrameSize.size != self._n: + def initFrameSizeGranular(self, initial_frame_size: Point): + if not initial_frame_size.is_all_defined() or initial_frame_size.size != self._n: raise IOError("GMesh: initFrameSizeGranular: inconsistent dimension of the frame size. \n" + - f"initial frame size defined: {initialFrameSize.is_all_defined()} \n" + - f"size: {initialFrameSize.size} \n" + + f"initial frame size defined: {initial_frame_size.is_all_defined()} \n" + + f"size: {initial_frame_size.size} \n" + f"n: {self._n}") self._frameSizeExp.reset(n=self._n) self._frameSizeMant.reset(n=self._n) - dMin: float = None + d_min: Optional[float] = None for i in range(self._n): if self._granularity[i] > 0: - dMin = self._granularity[i] + d_min = self._granularity[i] else: - dMin = 1 + d_min = 1 - div: float = initialFrameSize[i] / dMin + div: float = initial_frame_size[i] / d_min exp: int = self.roundFrameSizeExp(np.log10(abs(div))) self._frameSizeExp[i] = exp self._frameSizeMant[i] = self.roundFrameSizeMant(div*10**-exp) def roundFrameSizeExp(self, exp: float) -> int: - frameSizeExp: int = int(exp) - return frameSizeExp + frame_size_exp: int = int(exp) + return frame_size_exp def roundFrameSizeMant(self, mant: float): - frameSizeMant: int = 0 + frame_size_mant: int = 0 if mant < 1.5: - frameSizeMant = 1 + frame_size_mant = 1 elif mant >= 1.5 and mant < 3.5: - frameSizeMant = 2 + frame_size_mant = 2 else: - frameSizeMant = 5 + frame_size_mant = 5 - return frameSizeMant + return frame_size_mant - def getRho(self, i: int = None) -> auto: + def getRho(self, i: int = None) -> Any: if i is not None: - rho: auto + rho: Any diff: float = self._frameSizeExp[i] - self._initFrameSizeExp[i] - powDiff: float = 10.0 ** abs(diff) + pow_diff: float = 10.0 ** abs(diff) if self._granularity[i] > 0: - rho = self._frameSizeMant[i] * min(10.0**self._frameSizeExp[i], powDiff) + rho = self._frameSizeMant[i] * min(10.0**self._frameSizeExp[i], pow_diff) else: - rho = self._frameSizeMant[i] * powDiff + rho = self._frameSizeMant[i] * pow_diff else: rho: auto = [None] * self._n for i in range(self._n): diff: float = self._frameSizeExp[i] - self._initFrameSizeExp[i] - powDiff: float = 10.0 ** abs(diff) + pow_diff: float = 10.0 ** abs(diff) if self._granularity[i] > 0: - rho[i] = self._frameSizeMant[i] * min(10.0**self._frameSizeExp[i], powDiff) + rho[i] = self._frameSizeMant[i] * min(10.0**self._frameSizeExp[i], pow_diff) else: - rho[i] = self._frameSizeMant[i] * powDiff + rho[i] = self._frameSizeMant[i] * pow_diff return rho @@ -167,62 +165,62 @@ def getdeltaMeshSize(self, i: int = None) -> Point: return delta[i] def getDeltaFrameSize(self, i: int = None) -> Point: - dMinGran = 1.0 + d_min_gran = 1.0 Delta: Point = Point(self._n) Delta.coordinates = [0] * self._n if i is None: for i in range(self._n): if self._granularity[i] > 0: - dMinGran = self._granularity[i] - Delta[i] = dMinGran * self._frameSizeMant[i] * 10 ** self._frameSizeExp[i] + d_min_gran = self._granularity[i] + Delta[i] = d_min_gran * self._frameSizeMant[i] * 10 ** self._frameSizeExp[i] return Delta else: if self._granularity[i] > 0: - dMinGran = self._granularity[i] - Delta[i] = dMinGran * self._frameSizeMant[i] * 10 ** self._frameSizeExp[i] + d_min_gran = self._granularity[i] + Delta[i] = d_min_gran * self._frameSizeMant[i] * 10 ** self._frameSizeExp[i] return Delta[i] def getDeltaFrameSizeCoarser(self) -> Point: Delta: Point = Point(self._n) Delta.coordinates = [0] * self._n for i in range(self._n): - frameSizeMantOld = self._frameSizeMant[i] - frameSizeExpOld = self._frameSizeExp[i] - self._frameSizeMant[i], self._frameSizeExp[i] = self.getLargerMantExp(frameSizeMant=frameSizeMantOld, frameSizeExp=frameSizeExpOld, i=i) + frame_size_mant_old = self._frameSizeMant[i] + frame_size_exp_old = self._frameSizeExp[i] + self._frameSizeMant[i], self._frameSizeExp[i] = self.getLargerMantExp(frame_size_mant=frame_size_mant_old, i=i) Delta[i] = self.getDeltaFrameSize(i=i) - self._frameSizeMant[i] = frameSizeMantOld - self._frameSizeExp[i] = frameSizeExpOld + self._frameSizeMant[i] = frame_size_mant_old + self._frameSizeExp[i] = frame_size_exp_old return Delta - def getLargerMantExp(self, frameSizeMant: float, frameSizeExp: float, i: int): - if frameSizeMant == 1: + def getLargerMantExp(self, frame_size_mant: float, i: int): + if frame_size_mant == 1: self._frameSizeMant[i] = 2 - elif frameSizeMant == 2: + elif frame_size_mant == 2: self._frameSizeMant[i] = 5 else: self._frameSizeMant[i] = 1 self._frameSizeExp[i] += 1 return self._frameSizeMant[i], self._frameSizeExp[i] - def checkDeltasGranularity(self, i: int, deltaMeshSize: float, deltaFrameSize: float): + def checkDeltasGranularity(self, i: int, delta_mesh_size: float, delta_frame_size: float): if self._granularity[i] > 0.0: - hasError: bool = False + has_error: bool = False err: str = "Error: setDeltas: " - if not self.isMult(deltaMeshSize, self._granularity[i]): - hasError = True + if not self.isMult(delta_mesh_size, self._granularity[i]): + has_error = True err += f"deltaMeshSize at index {i}" err += f" is not a multiple of granularity {self._granularity[i]}" - elif not self.isMult(deltaFrameSize, self._granularity[i]): - hasError = True + elif not self.isMult(delta_frame_size, self._granularity[i]): + has_error = True err += f"deltaFrameSize at index {i}" err += f" is not a multiple of granularity {self._granularity[i]}" - if hasError: + if has_error: raise IOError(err) - def setDeltas(self, i: int, deltaMeshSize: float, deltaFrameSize: float): + def setDeltas(self, i: int = None, delta_mesh_size: float = None, delta_frame_size: float= None): # Input checks - self.checkDeltasGranularity(i=i, deltaMeshSize=deltaMeshSize, deltaFrameSize=deltaFrameSize) + self.checkDeltasGranularity(i=i, delta_mesh_size=delta_mesh_size, delta_frame_size=delta_frame_size) # Value to use for granularity (division so default = 1.0) gran: float = 1. if 0. < self._granularity[i]: @@ -233,9 +231,9 @@ def setDeltas(self, i: int, deltaMeshSize: float, deltaFrameSize: float): # Compute mantisse first # There are only 3 cases: 1, 2, 5, so compute all # 3 possibilities and then assign the values that work. - mant1: float = deltaFrameSize / (1.*gran) - mant2: float = deltaFrameSize / (2.*gran) - mant5: float = deltaFrameSize / (5. *gran) + mant1: float = delta_frame_size / (1.*gran) + mant2: float = delta_frame_size / (2.*gran) + mant5: float = delta_frame_size / (5. *gran) exp1: float = np.log10(mant1) exp2: float = np.log10(mant2) @@ -261,57 +259,50 @@ def setDeltas(self, i: int, deltaMeshSize: float, deltaFrameSize: float): # Sanity checks if self._enforceSanityChecks: - self.checkFrameSizeIntegrity(frameSizeExp=self._frameSizeExp[i], - frameSizeMant=self._frameSizeMant[i]) - self.checkSetDeltas(i=i, deltaMeshSize=deltaMeshSize, deltaFrameSize=deltaFrameSize) + self.checkFrameSizeIntegrity(frame_size_exp=self._frameSizeExp[i], + frame_size_mant=self._frameSizeMant[i]) + self.checkSetDeltas(i=i, delta_mesh_size=delta_mesh_size, delta_frame_size=delta_frame_size) self.checkDeltasGranularity(i, self.getdeltaMeshSize(i=i), self.getDeltaFrameSize(i=i)) - - - - - - - def checkFrameSizeIntegrity(self, frameSizeExp: float, frameSizeMant: float): + def checkFrameSizeIntegrity(self, frame_size_exp: float, frame_size_mant: float): # frameSizeExp must be an integer. # frameSizeMant must be 1, 2 or 5. - hasError: bool = False + has_error: bool = False err: str = "Error: Integrity check" - if not isinstance(frameSizeExp, int): - hasError = True - err += f" of frameSizeExp ({frameSizeExp}): Should be integer." - elif (frameSizeMant != 1.0 and frameSizeMant != 2.0 and frameSizeMant != 5.0): - hasError = True - err += f" of frameSizeMant ({frameSizeMant}): Should be integer." - - if hasError: + if not isinstance(frame_size_exp, int): + has_error = True + err += f" of frameSizeExp ({frame_size_exp}): Should be integer." + elif (not np.isclose(frame_size_mant, 1.0, rtol=1e-09, atol=1e-09) and not np.isclose(frame_size_mant, 2.0, rtol=1e-09, atol=1e-09) and not np.isclose(frame_size_mant, 5.0, rtol=1e-09, atol=1e-09)): + has_error = True + err += f" of frameSizeMant ({frame_size_mant}): Should be integer." + + if has_error: raise IOError(err) - def checkSetDeltas(self, i: int, deltaMeshSize: float, deltaFrameSize: float): - hasError: bool = False + def checkSetDeltas(self, i: int, delta_mesh_size: float, delta_frame_size: float): + has_error: bool = False err: str = "Warning: setDeltas did not give good value" # Something might be wrong with setDeltas(), so double check. - if self.getdeltaMeshSize(i=i) != deltaMeshSize: - hasError = True + if self.getdeltaMeshSize(i=i) != delta_mesh_size: + has_error = True err += f" for deltaMeshSize at index {i}" - err += f" Expected: {deltaMeshSize}" + err += f" Expected: {delta_mesh_size}" err += f" computed: {self.getdeltaMeshSize(i=i)}" - elif self.getDeltaFrameSize(i=i) != deltaFrameSize: - hasError = True + elif self.getDeltaFrameSize(i=i) != delta_frame_size: + has_error = True err += f" for deltaFrameSize at index {i}" - err += f" Expected: {deltaFrameSize}" + err += f" Expected: {delta_frame_size}" err += f" computed: {self.getDeltaFrameSize(i=i)}" - if (hasError): + if (has_error): raise IOError(err) - - - def scaleAndProjectOnMesh(self, dir: Point): + + def scaleAndProjectOnMesh(self, dir: Point = None): proj: Point = Point(self._n) - infiniteNorm: float = np.linalg.norm(dir.coordinates, np.inf) + infinite_norm: float = np.linalg.norm(dir.coordinates, np.inf) - if 0 == infiniteNorm: + if 0 == infinite_norm: err = "GMesh: scaleAndProjectOnMesh: Cannot handle an infinite norm of zero" raise IOError(err) @@ -319,111 +310,109 @@ def scaleAndProjectOnMesh(self, dir: Point): if self._frameSizeMant.is_all_defined() and self._frameSizeExp.is_all_defined(): for i in range(self._n): - delta: float = self.getdeltaMeshSize(i=i) - proj[i] = np.round(self.getRho(i=i)*dir[i]/infiniteNorm) * delta + delta: Any = self.getdeltaMeshSize(i=i) + proj[i] = np.round(self.getRho(i=i)*dir[i]/infinite_norm) * delta else: err = "GMesh: scaleAndProjectOnMesh cannot be performed." err += f" i = {i}" err += f" mantissa defined: {self._frameSizeMant.is_all_defined()}" err += f" exp defined: {self._frameSizeExp.is_all_defined()}" - err += f"delta mesh size defined: {delta}" + err += f"delta mesh size defined: {self.getdeltaMeshSize()}" raise IOError(err) return proj - def projectOnMesh(self, point: Point, frameCenter: Point): + def projectOnMesh(self, point: Point, frame_center: Point): proj: Point = point - delta: auto = self.getdeltaMeshSize() - maxNbTry: int = 10 - verifValueI: Point = Point(self._n) - verifValueI.coordinates = [0] * self._n + delta: Any = self.getdeltaMeshSize() + max_nb_try: int = 10 + verif_value_i: Point = Point(self._n) + verif_value_i.coordinates = [0] * self._n for i in range(point.size): - deltaI = delta[i] - frameCenterIsOnMesh: bool = self.isMult(frameCenter[i], deltaI) + delta_i = delta[i] + frame_center_is_on_mesh: bool = self.isMult(frame_center[i], delta_i) - diffProjFrameCenter: float = proj[i] - frameCenter[i] - verifValueI[i] = proj[i] if (frameCenterIsOnMesh) else diffProjFrameCenter + diff_proj_frame_center: float = proj[i] - frame_center[i] + verif_value_i[i] = proj[i] if (frame_center_is_on_mesh) else diff_proj_frame_center # // Force verifValueI to be a multiple of deltaI. # // nbTry = 0 means point is already on mesh. # // nbTry = 1 means the projection worked. # // nbTry > 1 means the process went hacky by forcing the value to work # // for verifyPointIsOnMesh. - nbTry = 0 - while (not self.isMult(verifValueI[i], deltaI) and nbTry <= maxNbTry): - newVerifValueI: float - if (0==nbTry): + nb_try = 0 + while (not self.isMult(verif_value_i[i], delta_i) and nb_try <= max_nb_try): + new_verif_value_i: float + if (0==nb_try): # Use closest projection - vHigh = verifValueI.nextMult(deltaI, i) + v_high = verif_value_i.next_mult(delta_i, i) p: Point = Point(self._n) - p.coordinates = [-c for c in verifValueI.coordinates] - vLow = - (p.nextMult(deltaI, i)) - diffHigh = vHigh - verifValueI[i] - diffLow = verifValueI[i] - vLow - verifValueI[i] = vLow if (diffLow < diffHigh) else (vHigh if (diffHigh < diffLow) else (vLow if (proj[i] < 0) else vHigh)) + p.coordinates = [-c for c in verif_value_i.coordinates] + v_low = - (p.next_mult(delta_i, i)) + diff_high = v_high - verif_value_i[i] + diff_low = verif_value_i[i] - v_low + verif_value_i[i] = v_low if (diff_low < diff_high) else (v_high if (diff_high < diff_low) else (v_low if (proj[i] < 0) else v_high)) else: p: Point = Point(self._n) - p.coordinates = [-c for c in verifValueI.coordinates] - verifValueI[i] = verifValueI.nextMult(deltaI, i) if (diffProjFrameCenter >= 0) else (-(p.nextMult(deltaI, i))) - proj[i] = verifValueI[i] if frameCenterIsOnMesh else verifValueI[i] + frameCenter[i] + p.coordinates = [-c for c in verif_value_i.coordinates] + verif_value_i[i] = verif_value_i.next_mult(delta_i, i) if (diff_proj_frame_center >= 0) else (-(p.next_mult(delta_i, i))) + proj[i] = verif_value_i[i] if frame_center_is_on_mesh else verif_value_i[i] + frame_center[i] # Recompute verifValue for more precision - newVerifValueI = proj[i] if frameCenterIsOnMesh else proj[i] - frameCenter[i] - nbTry += 1 + new_verif_value_i = proj[i] if frame_center_is_on_mesh else proj[i] - frame_center[i] + nb_try += 1 # Special cases - while (newVerifValueI != verifValueI[i] and nbTry <= maxNbTry): - if verifValueI[i] >= 0: - verifValueI[i] = max(verifValueI[i], newVerifValueI) - verifValueI[i] += self.dtype.zero - verifValueI[i] = verifValueI.nextMult(deltaI, i) + while (new_verif_value_i != verif_value_i[i] and nb_try <= max_nb_try): + if verif_value_i[i] >= 0: + verif_value_i[i] = max(verif_value_i[i], new_verif_value_i) + verif_value_i[i] += self.dtype.zero + verif_value_i[i] = verif_value_i.next_mult(delta_i, i) else: - verifValueI[i] = min(verifValueI[i], newVerifValueI) - verifValueI[i] -= self.dtype.zero + verif_value_i[i] = min(verif_value_i[i], new_verif_value_i) + verif_value_i[i] -= self.dtype.zero p: Point = Point(self._n) - p.coordinates = [-c for c in verifValueI.coordinates] - verifValueI[i] = -(p.nextMult(deltaI, i)) - proj[i] = verifValueI[i] if frameCenterIsOnMesh else verifValueI[i] + frameCenter[i] + p.coordinates = [-c for c in verif_value_i.coordinates] + verif_value_i[i] = -(p.next_mult(delta_i, i)) + proj[i] = verif_value_i[i] if frame_center_is_on_mesh else verif_value_i[i] + frame_center[i] # Recompute verifValue for more precision - newVerifValueI = proj[i] if frameCenterIsOnMesh else proj[i] - frameCenter[i] - nbTry += 1 + new_verif_value_i = proj[i] if frame_center_is_on_mesh else proj[i] - frame_center[i] + nb_try += 1 - verifValueI[i] = newVerifValueI + verif_value_i[i] = new_verif_value_i - if (nbTry >= maxNbTry and not self.isMult(verifValueI[i], deltaI)): + if (nb_try >= max_nb_try and not self.isMult(verif_value_i[i], delta_i)): # TODO: print warning proj[i] = point[i] return proj - def check_min_poll_size_criterion (self) -> bool: """ Check the minimal poll size criterion. """ if not self._Delta_min_is_defined: return False - S, D = self.get_Delta_object() + S, _ = self.get_Delta_object() return S - def check_min_mesh_size_criterion (self) -> bool: + def check_min_mesh_size_criterion(self) -> bool: """ Check the minimal mesh size criterion. """ if not self._delta_min.is_all_defined(): return False - S, D = self.get_delta_object() + S, _ = self.get_delta_object() return S - def get_rho (self, i: int): + def get_rho(self, i: int): """ Access to the ratio of poll size / mesh size parameter rho^k. :param rho The ratio poll/mesh size rho^k -- OUT. """ - rho: float = None + rho: Optional[float] = None if self._granularity[i] > 0: rho = self._frameSizeMant.coordinates[i] * min(10** self._frameSizeExp.coordinates[i], 10**abs(self._frameSizeExp.coordinates[i]-self._initFrameSizeExp.coordinates[i])) else: rho = self._frameSizeMant.coordinates[i] * 10** abs(self._frameSizeExp.coordinates[i]-self._initFrameSizeExp.coordinates[i]) return rho - - def get_delta (self, i: int): + def get_delta(self, i: int): """ Access to the mesh size parameter delta^k. :param delta: The mesh size parameter delta^k -- OUT. @@ -434,7 +423,7 @@ def get_delta (self, i: int): delta = self._granularity[i] * max(1.0, delta) return delta - def get_Delta (self, i: int): + def get_Delta(self, i: int): """ Access to the poll size parameter Delta^k. :param Delta: The poll size parameter Delta^k -- OUT. @@ -459,7 +448,7 @@ def init(self): self._finestMeshSize = self.getdeltaMeshSize() for i in range(self._n): - if 0.0 == self._granularity[i]: + if np.isclose(0.0, self._granularity[i], rtol=1e-09, atol=1e-09): self._allGranular = False break @@ -468,68 +457,67 @@ def init(self): if self._enforceSanityChecks: for i in range(self._n): - self.checkFrameSizeIntegrity(frameSizeExp=self._frameSizeExp[i], frameSizeMant=self._frameSizeMant[i]) - self.checkDeltasGranularity(i=i, deltaMeshSize=self.getdeltaMeshSize(i=i), deltaFrameSize=self.getDeltaFrameSize(i=i)) + self.checkFrameSizeIntegrity(frame_size_exp=self._frameSizeExp[i], frame_size_mant=self._frameSizeMant[i]) + self.checkDeltasGranularity(i=i, delta_mesh_size=self.getdeltaMeshSize(i=i), delta_frame_size=self.getDeltaFrameSize(i=i)) def isMult(self, v1, v2)->bool: return ((v1%v2) <= self.dtype.zero) - def enlargeDeltaFrameSize(self, direction: Point) -> bool: - oneFrameSizeChanged = False - minRho = np.inf + def enlargeDeltaFrameSize(self, direction: Point = None) -> bool: + one_frame_size_changed = False + min_rho = np.inf for i in range(self._n): if self._granularity[i] == 0: - minRho = min(minRho, self.getRho(i=i)) + min_rho = min(min_rho, self.getRho(i=i)) for i in range(self._n): - frameSizeIChanged = False - if (not self._anisotropicMesh or abs(direction[i])/self.getdeltaMeshSize(i=i)/self.getRho(i=i) > self._anisotropyFactor or (self._granularity[i] == 0 and self._frameSizeExp[i] < self._initFrameSizeExp[i] and self.getRho(i=i) > minRho*minRho)): - self.getLargerMantExp(frameSizeMant=self._frameSizeMant[i], frameSizeExp=self._frameSizeExp[i], i=i) - frameSizeIChanged = True - oneFrameSizeChanged = True + frame_size_i_changed = False + if (not self._anisotropicMesh or abs(direction[i])/self.getdeltaMeshSize(i=i)/self.getRho(i=i) > self._anisotropyFactor or (self._granularity[i] == 0 and self._frameSizeExp[i] < self._initFrameSizeExp[i] and self.getRho(i=i) > min_rho*min_rho)): + self.getLargerMantExp(frame_size_mant=self._frameSizeMant[i], i=i) + frame_size_i_changed = True + one_frame_size_changed = True # update the mesh index self._r[i] += 1 self._rMax[i] = max(self._r[i], self._rMax[i]) # Sanity checks - if self._enforceSanityChecks and frameSizeIChanged: + if self._enforceSanityChecks and frame_size_i_changed: self.checkFrameSizeIntegrity(self._frameSizeExp[i], self._frameSizeMant[i]) - self.checkDeltasGranularity(i=i, deltaMeshSize=self.getdeltaMeshSize(i=i), deltaFrameSize=self.getDeltaFrameSize(i=i)) + self.checkDeltasGranularity(i=i, delta_mesh_size=self.getdeltaMeshSize(i=i), delta_frame_size=self.getDeltaFrameSize(i=i)) # When we enlarge the frame size we may keep the mesh size unchanged. So we need to test. msize = self.getdeltaMeshSize() if self._finestMeshSize < msize: self._isFinest = False - return oneFrameSizeChanged - - def refineDeltaFrameSizeME(self, frameSizeMant: float, frameSizeExp:float, granularity: float): - if frameSizeMant == 1: - frameSizeMant = 5 - frameSizeExp -= 1 - elif frameSizeMant == 2: - frameSizeMant = 1 + return one_frame_size_changed + + def refineDeltaFrameSizeME(self, frame_size_mant: float, frame_size_exp:float, granularity: float): + if frame_size_mant == 1: + frame_size_mant = 5 + frame_size_exp -= 1 + elif frame_size_mant == 2: + frame_size_mant = 1 else: - frameSizeMant = 2 + frame_size_mant = 2 # When the mesh reaches granularity (exp = 1, mant = 1), make sure to remove the refinement - if granularity > 0 and frameSizeExp < 0 and frameSizeMant == 5: - frameSizeExp = 0 - frameSizeMant = 1 + if granularity > 0 and frame_size_exp < 0 and frame_size_mant == 5: + frame_size_exp = 0 + frame_size_mant = 1 - return frameSizeMant, frameSizeExp + return frame_size_mant, frame_size_exp - def getdeltaMeshSizeF(self, frameSizeExp:int, initFrameSizeExp:int, granularity: int)->float: - diff = frameSizeExp - initFrameSizeExp - exp = frameSizeExp - abs(diff) + def getdeltaMeshSizeF(self, frame_size_exp:int, init_frame_size_exp:int, granularity: int)->float: + diff = frame_size_exp - init_frame_size_exp + exp = frame_size_exp - abs(diff) delta = 10.0**exp if 0.0 < granularity: delta = granularity * max(1.0, delta) return delta - - def refineDeltaFrameSize(self) -> bool: + def refineDeltaFrameSize(self): # // Compute the new values frameSizeMant and frameSizeExp first. # // We will do some verifications before setting them. self._refineCount += 1 @@ -539,31 +527,31 @@ def refineDeltaFrameSize(self) -> bool: for i in range(self._n): # // Compute the new values frameSizeMant and frameSizeExp first. # // We will do some verifications before setting them. - frameSizeMant = self._frameSizeMant[i] - frameSizeExp = self._frameSizeExp[i] - frameSizeMant, frameSizeExp= self.refineDeltaFrameSizeME(frameSizeMant=frameSizeMant, frameSizeExp=frameSizeExp, granularity=self._granularity[i]) + frame_size_mant = self._frameSizeMant[i] + frame_size_exp = self._frameSizeExp[i] + frame_size_mant, frame_size_exp= self.refineDeltaFrameSizeME(frame_size_mant=frame_size_mant, frame_size_exp=frame_size_exp, granularity=self._granularity[i]) # Verify delta mesh size does not go too small if we use the new values. - olddeltaMeshSize = self.getdeltaMeshSizeF(frameSizeExp=self._frameSizeExp[i], initFrameSizeExp=self._initFrameSizeExp[i], granularity=self._granularity[i]) - if self._minMeshSize[i] <= olddeltaMeshSize: + old_delta_mesh_size = self.getdeltaMeshSizeF(frame_size_exp=self._frameSizeExp[i], init_frame_size_exp=self._initFrameSizeExp[i], granularity=self._granularity[i]) + if self._minMeshSize[i] <= old_delta_mesh_size: # update mesh index if self._granularity[i] == 0: self._r[i] -= 1 else: # Update mesh index if not already at the min limit. When refining the frame, if mantissa and exponent stay the same, the min limit is reached (do not decrease). - if (not (self._frameSizeMant[i] == frameSizeMant and self._frameSizeExp[i] == frameSizeExp)): + if (not (self._frameSizeMant[i] == frame_size_mant and self._frameSizeExp[i] == frame_size_exp)): self._r[i] -= 1 # Update the minimal mesh index reached so far self._rMin[i] = min(self._r[i], self._rMin[i]) # We can go lower - self._frameSizeMant[i] = frameSizeMant - self._frameSizeExp[i] = frameSizeExp + self._frameSizeMant[i] = frame_size_mant + self._frameSizeExp[i] = frame_size_exp # Sanity checks if self._enforceSanityChecks: - self.checkFrameSizeIntegrity(frameSizeExp=self._frameSizeExp[i], - frameSizeMant=self._frameSizeMant[i]) - self.checkDeltasGranularity(i=i, deltaMeshSize=self.getdeltaMeshSize(i=i), deltaFrameSize=self.getDeltaFrameSize(i=i)) + self.checkFrameSizeIntegrity(frame_size_exp=self._frameSizeExp[i], + frame_size_mant=self._frameSizeMant[i]) + self.checkDeltasGranularity(i=i, delta_mesh_size=self.getdeltaMeshSize(i=i), delta_frame_size=self.getDeltaFrameSize(i=i)) msize = self.getdeltaMeshSize() if msize <= self._finestMeshSize: self._isFinest = True @@ -575,302 +563,6 @@ def update(self): return -# ############################################### -# ############################################### -# ############################################### - - # def init_poll_size_granular (self, cont_init_poll_size: Point ): - # """ - # :param: cont_init_poll_size: continuous initial poll size -- IN. - # """ - - # if not all(cont_init_poll_size.defined) or cont_init_poll_size.n_dimensions != self._n: - # raise IOError("Inconsistent dimension of the poll size!") - - # self._frameSizeExp.reset(n=self._n) - # self._frameSizeMant.reset(n=self._n) - # self._pos_mant_0.reset(n=self._n) - - # d_min: float - - # for i in range(self._n): - # if self._granularity.defined[i] and self._granularity.coordinates[i] > 0: - # d_min = self._granularity[i] - # else: - # d_min=1.0 - - # exp: int = int(np.log10(abs(cont_init_poll_size.coordinates[i]/d_min))) - # if exp < 0: - # exp = 0 - - # self._frameSizeExp.coordinates[i]=exp - # cont_mant: float = cont_init_poll_size.coordinates[i] / d_min * 10.0**(-exp) - - # if cont_mant < 1.5: - # self._frameSizeMant.coordinates[i] = 1 - # self._pos_mant_0[i] = 0 - # elif (cont_mant >= 1.5 and cont_mant < 3.5): - # self._frameSizeMant.coordinates[i] = 2 - # self._pos_mant_0.coordinates[i] = 1 - # else: - # self._frameSizeMant.coordinates[i] = 5 - # self._pos_mant_0.coordinates[i] = 2 - - - - # def get_delta_object(self): - # """ """ - # stop = True - # delta: Point = Point(self._n) - # for i in range(self._n): - # delta.coordinates[i] = self.get_delta(i=i) - # if stop and self._delta_min_is_defined and not self._fixed_variables.defined[i] and self._delta_min.defined[i] and delta.coordinates[i] >= self._delta_min[i]: - # stop = False - # return stop, delta - - # def get_delta_max(self)->Point: - # return self._delta_0 - - # def get_Delta_object(self)->Point: - # """ """ - # stop = True - # Delta: Point = Point(self._n) - # for i in range(self._n): - # Delta.coordinates[i] = self.get_Delta(i=i) - # if stop and self._granularity.coordinates[i] == 0 and not self._fixed_variables.defined[i] and (self._Delta_min_is_complete or Delta.coordinates[i] >= self._Delta_min[i]): - # stop = False - - # if stop and self._granularity.coordinates[i] > 0 and not self._fixed_variables.defined[i] and (not self._Delta_min_is_complete or Delta.coordinates[i] > self._Delta_min[i]): - # stop = False - - - # return stop, Delta - - # def is_finer_than_initial(self): - # """ """ - # for i in range(self._n): - # if not self._fixed_variables.defined[i]: - # # For continuous variables - # if self._granularity.coordinates[i]==0 and (self._frameSizeExp.coordinates[i] > self._initFrameSizeExp.coordinates[i] or ( self._frameSizeExp.coordinates[i] == self._initFrameSizeExp.coordinates[i] and self._frameSizeMant.coordinates[i] >= self._Delta_0_mant.coordinates[i] )): - # return False - # # For granular variables (case 1) - # if self._granularity.coordinates[i] > 0 and (self._frameSizeExp.coordinates[i] > self._initFrameSizeExp.coordinates[i] or ( self._frameSizeExp.coordinates[i] == self._initFrameSizeExp.coordinates[i] and self._frameSizeMant.coordinates[i] > self._Delta_0_mant.coordinates[i] )): - # return False - # # For continuous variables (case 2) - # if self._granularity.coordinates[i]>0 and (self._frameSizeExp.coordinates[i] == self._initFrameSizeExp.coordinates[i] and self._frameSizeMant.coordinates[i] == self._Delta_0_mant.coordinates[i] and (self._frameSizeExp.coordinates[i] != 0 or self._frameSizeMant.coordinates[i] != 1) ): - # return False - - # return True - - # def update(self, success: SUCCESS_TYPES, d: List[float]): - # if d and self._n != len(d): - # raise IOError("delta_0 and d have different sizes") - - # if success == SUCCESS_TYPES.FS: - # for i in range(self._n): - # if (self._granularity.coordinates[i] == 0 and not self._fixed_variables.defined[i]): - # if i > 0: - # min_rho = min(min_rho, self.get_rho(i)) - # else: - # min_rho = self.get_rho(i) - - # for i in range(self._n): - # if (not d or not self._anisotropic_mesh or abs(d[i])/self.get_delta(i)/self.get_rho(i) > self._anisotropic_factor or ( self._granularity.coordinates[i] == 0 and self._frameSizeExp.coordinates[i] < self._initFrameSizeExp.coordinates[i] and self.get_rho(i) > min_rho*min_rho )): - # # Update the mesh index - # self._r.coordinates[i] += 1 - # self._r_max.coordinates[i] = max(self._r.coordinates[i], self._r_max.coordinates[i]) - # # update the mantissa and exponent - # if ( self._frameSizeMant.coordinates[i] == 1 ): - # self._frameSizeMant.coordinates[i]= 2 - # elif ( self._frameSizeMant.coordinates[i] == 2 ): - # self._frameSizeMant.coordinates[i]=5 - # else: - # self._frameSizeMant.coordinates[i]=1 - # self._frameSizeExp.coordinates[i] += 1 - # elif success == SUCCESS_TYPES.US: - # for i in range(self._n): - # if (not self._fixed_variables.defined[i]): - # # update the mesh index - # self._r.coordinates[i] -= 1 - # # update the mesh mantissa and exponent - # if (self._frameSizeMant.coordinates[i]==1): - # self._frameSizeMant.coordinates[i] = 5 - # self._frameSizeExp.coordinates[i] -= 1 - # elif self._frameSizeMant.coordinates[i] == 2: - # self._frameSizeMant.coordinates[i] = 1 - # else: - # self._frameSizeMant.coordinates[i] = 2 - - # if ( self._granularity.coordinates[i] > 0 and self._frameSizeExp.coordinates[i]==-1 and self._frameSizeMant.coordinates[i]==5 ): - # self._r.coordinates[i] += 1 - # self._frameSizeExp.coordinates[i]=0 - # self._frameSizeMant.coordinates[i]=1 - # self._r_min.coordinates[i] = min(self._r.coordinates[i], self._r_min.coordinates[i]) - - # # for i in range(self._n): - # # # Test for producing anisotropic mesh + correction to prevent mesh collapsing for some variables ( ifnot ) - # # if (not d or not self._anisotropic_mesh or d[i]/self.get_delta(i)): - - - # def reset(self): - # """ """ - # self.__init__() - - # def is_finest(self): - # """ """ - # for i in range(self._n): - # if not self._fixed_variables.defined[i] and self._r.coordinates[i] > self._r_min.coordinates[i]: - # return False - # return True - - - - # def scale_and_project(self, i: int, l: float, round_up: bool): - # """ """ - # delta: float = self.get_delta(i=i) - # if i<= self._n and self._frameSizeMant.is_all_defined() and self._frameSizeExp.is_all_defined() and delta is not None: - # d: float = self.get_rho(i=i) * l - # # round to double - # return np.round(d)*delta - # else: - # raise IOError("scale_and_project(): mesh scaling and projection cannot be performed!") - - - - - # def check_min_mesh_sizes(self, stop: bool=None, stop_reason: STOP_TYPE = None): - # """_summary_ - # """ - # if stop: - # return - - # stop = False - # # Coarse mesh stopping criterion - # for i in range(self._n): - # if self._r.coordinates[i] > -GL_LIMITS: - # stop = True - # break - # if stop: - # stop_reason = STOP_TYPE.GL_LIMITS_REACHED - # return - - # stop = True - - # # // Fine mesh stopping criterion. Do not apply when all variables have granularity. - # # // To trigger this stopping criterion: - # # // - All mesh indices must be < _limit_mesh_index for all continuous variables (granularity==0), and - # # // - mesh size == granularity for all granular variables. - # if self._all_granular: - # stop = False - - # else: - # for i in range(self._n): - # # Skip fixed variables - # if self._fixed_variables.defined[i]: - # continue - # # Do not stop if the mesh size of a variable is strictly larger than its granularity - # if self._granularity.coordinates[i] > 0 and self.get_delta(i=i) > self._granularity.coordinates[i]: - # stop = False - # break - # # Do not stop if the mesh of a variable is above the limit mesh index - # if self._granularity.coordinates[i] == 0 and self._r.coordinates[i] >= self._granularity.coordinates[i]: - # stop = False - # break - - # if stop: - # stop_reason = STOP_TYPE.GL_LIMITS_REACHED - # return - - # # 2. delta^k (mesh size) tests: - # if self.check_min_poll_size_criterion(): - # stop = True - # stop_reason = STOP_TYPE.DELTA_P_MIN_REACHED - # return - - # # 3. delta^k (mesh size) tests: - # if self.check_min_mesh_size_criterion(): - # stop = True - # stop_reason = STOP_TYPE.DELTA_M_MIN_REACHED - # return - - - - - # def get_mesh_indices(self): - # """_summary_ - # """ - # return self._r - - - # def get_min_mesh_indices(self): - # """_summary_ - # """ - # return self._r_min - - # def get_max_mesh_indices(self): - # """_summary_ - # """ - # return self._r_max - - # def set_mesh_indices(self, r: Point): - # """_summary_ - # """ - # if r.size != self._n: - # raise IOError("set_mesh_indices(): dimension of provided mesh indices must be consistent with their previous dimension") - - # if r.coordinates[0] < HARD_MIN_MESH_INDEX: - # raise IOError("set_mesh_indices(): mesh index is too small") - - # # Set the mesh indices - # self._r = copy.deepcopy(r) - # for i in range(self._n): - # if (r.coordinates[i]>self._r_max.coordinates[i]): - # self._r_max.coordinates[i] = r.coordinates[i] - # if (r.coordinates[i] < self._r_min.coordinates[i]): - # self._r_min.coordinates[i] = r.coordinates[i] - - # # Set the mesh mantissas and exponents according to the mesh indices - # for i in range(self._n): - # shift: int = int(self._r.coordinates[i] + self._pos_mant_0.coordinates[i]) - # pos: int = self.isMult((shift + 300), 3) - - # self._frameSizeExp.coordinates[i] = np.floor((shift+300.0)/3.0) - 100.0 + self._initFrameSizeExp.coordinates[i] - - # if pos == 0: - # self._frameSizeMant.coordinates[i] = 1 - # elif pos == 1: - # self._frameSizeMant.coordinates[i] = 2 - # elif pos == 2: - # self._frameSizeMant.coordinates[i] = 5 - # else: - # raise IOError("set_mesh_indices(): something is wrong with conversion from index to mantissa and exponent") - - # def set_limit_mesh_index(self, l: int): - # """_summary_ - # """ - # if l > 0: - # raise IOError("set_limit_mesh_index(): the limit mesh index must be negative or null.") - - # if l > HARD_MIN_MESH_INDEX: - # raise IOError("set_limit_mesh_index(): the limit mesh index is too small.") - - # self._limit_mesh_index = l - - - - # def get_mesh_ratio_if_success(self): - # """_summary_ - # """ - # ratio: Point = Point(self._n) - # for i in range(self._n): - # power_of_tau: float = self._update_basis**(0 if self._r.coordinates[i] >= 0 else 2*self._r.coordinates[i]) - - # power_of_tau_if_success: float = self._update_basis**(0 if self._r.coordinates[i]+self._coarsening_step >= 0 else 2*(self._r.coordinates[i]+self._coarsening_step)) - - # ratio.coordinates[i] = power_of_tau_if_success/power_of_tau - - # return ratio - diff --git a/src/OMADS/MADS.py b/src/OMADS/MADS.py index a2743e5..afc4fc0 100644 --- a/src/OMADS/MADS.py +++ b/src/OMADS/MADS.py @@ -30,10 +30,12 @@ import sys import OMADS.POLL as PS import OMADS.SEARCH as SS -from typing import List, Dict, Any +from .Exploration import efficient_exploration +from .Directions import Dirs2n +from typing import List, Dict, Any, Optional import numpy as np if importlib.util.find_spec('BMDFO'): - from BMDFO import toy + from BMDFO import toy # type: ignore import time from .Point import Point from .CandidatePoint import CandidatePoint @@ -50,27 +52,27 @@ @dataclass class MADS: - search: SS.efficient_exploration = None - search_VN: SS.VNS = None - poll: PS.Dirs2n = None - param: Parameters = None - evaluator: Evaluator = None - post: PostMADS = None - out: Output = None - outP: Output = None - options: Options = None - data: dict = None - xmin: CandidatePoint = None + search: Optional[efficient_exploration] = None + search_vns: SS.VNS = None + poll: Optional[Dirs2n] = None + param: Optional[Parameters] = None + evaluator: Optional[Evaluator] = None + post: Optional[PostMADS] = None + out: Optional[Output] = None + out_p: Optional[Output] = None + options: Optional[Options] = None + data: Optional[dict] = None + xmin: Optional[CandidatePoint] = None iteration: int = 0 peval: int = 0 HT: Any = None - log: logger = None + log: Optional[logger] = None B: Any = None - LAMBDA_k: float = 0. - RHO_k: float = 0. + lambda_multipliers_k: float = 0. + rho_k: float = 0. tic: Any = None toc: Any = None - active_barrier: Barrier = None + active_barrier: Optional[Barrier] = None def __init__(self, data: dict): """ Initialize the log file """ @@ -78,16 +80,16 @@ def __init__(self, data: dict): if not os.path.exists(data["param"]["post_dir"]): try: os.mkdir(data["param"]["post_dir"]) - except: + except Warning: os.makedirs(data["param"]["post_dir"], exist_ok=True) self.log.initialize(data["param"]["post_dir"] + "/OMADS.log") - self.log.log_msg(msg="Preprocess the MADS algorithim...", msg_type=PS.MSG_TYPE.INFO) - self.log.log_msg(msg="Preprocess the search step...", msg_type=PS.MSG_TYPE.INFO) + self.log.log_msg(msg="Preprocess the MADS algorithim...", msg_type=MSG_TYPE.INFO) + self.log.log_msg(msg="Preprocess the search step...", msg_type=MSG_TYPE.INFO) _, _, self.search, _, _, _, _, _, _ = SS.PreExploration(data).initialize_from_dict(log=self.log) - self.log.log_msg(msg="Preprocess the POLL step...", msg_type=PS.MSG_TYPE.INFO) - self.iteration, self.xmin, self.poll, self.options, self.param, self.post, self.out, self.B, self.outP = PS.PrePoll(data).initialize_from_dict(log=self.log, xs=self.search.xmin) - self.out.stepName = "Poll" + self.log.log_msg(msg="Preprocess the POLL step...", msg_type=MSG_TYPE.INFO) + self.iteration, self.xmin, self.poll, self.options, self.param, self.post, self.out, self.B, self.out_p = PS.PrePoll(data).initialize_from_dict(log=self.log, xs=self.search.xmin) + self.out.step_name = "Poll" self.post.step_name = [f'Search: {self.search.type}'] self.HT = copy.deepcopy(self.poll.hashtable) @@ -99,8 +101,8 @@ def search_step(self, xmin: SS.CandidatePoint=None): self.search.log = self.log self.search.xmin = xmin self.search.mesh.update() - self.search.LAMBDA = self.LAMBDA_k - self.search.RHO = self.RHO_k + self.search.LAMBDA = self.lambda_multipliers_k + self.search.RHO = self.rho_k B = self.active_barrier if self.HT is not None: self.search.hashtable = self.HT @@ -111,54 +113,38 @@ def search_step(self, xmin: SS.CandidatePoint=None): B.select_poll_center() B.update_and_reset_success() elif isinstance(B, BarrierMO) and self.iteration == 1: - B.init(evalPointList=[xmin]) + B.init(eval_point_list=[xmin]) - - # search.hmax = B._h_max - - if isinstance(B, Barrier): + + if isinstance(B, Barrier) or isinstance(B, BarrierMO): self.search.hmax = B._h_max - # TODO: Check whether the commented code below is needed - # if xmin.status == DESIGN_STATUS.FEASIBLE: - # B.insert_feasible(search.xmin) - # elif xmin.status == DESIGN_STATUS.INFEASIBLE: - # B.insert_infeasible(search.xmin) - # else: - # B.insert(search.xmin) - elif isinstance(B, BarrierMO): - self.search.hmax = B._hMax + # COMPLETED: Check whether the commented code below is needed """ Create the set of poll directions """ - if self.search.type == SS.SEARCH_TYPE.VNS.name and self.search_VN is not None: - self.search_VN.active_barrier = B - self.search._candidate_points_set = self.search_VN.run() - if self.search_VN.stop: + if self.search.type == SS.SEARCH_TYPE.VNS.name and self.search_vns is not None: + self.search_vns.active_barrier = B + self.search._candidate_points_set = self.search_vns.run() + if self.search_vns.stop: print("Reached maximum number of VNS iterations!") self.HT = self.search.hashtable - self.RHO_k = self.search.RHO + self.rho_k = self.search.RHO self.active_barrier = B - self.LAMBDA_k = self.search.LAMBDA + self.lambda_multipliers_k = self.search.LAMBDA return self.search.xmin self.search.map_samples_from_coords_to_points(samples=self.search._candidate_points_set) else: - vvp = vvs = [] - bestFeasible: CandidatePoint = B._currentIncumbentFeas if isinstance(B, BarrierMO) else B._best_feasible - bestInf: CandidatePoint = B._currentIncumbentInf if isinstance(B, BarrierMO) else B.get_best_infeasible() - if bestFeasible is not None and bestFeasible.evaluated: - self.search.xmin = bestFeasible - vvp, _ = self.search.generate_sample_points(int(((self.search.dim+1)/2)*((self.search.dim+2)/2)) if self.search.ns is None else self.search.ns) - if bestInf is not None and bestInf.evaluated: + best_feasible: CandidatePoint = B._currentIncumbentFeas if isinstance(B, BarrierMO) else B._best_feasible + best_inf: CandidatePoint = B._currentIncumbentInf if isinstance(B, BarrierMO) else B.get_best_infeasible() + if best_feasible is not None and best_feasible.evaluated: + self.search.xmin = best_feasible + self.search.generate_sample_points(int(((self.search.dim+1)/2)*((self.search.dim+2)/2)) if self.search.ns is None else self.search.ns) + if best_inf is not None and best_inf.evaluated: # if B._filter is not None and B.get_best_infeasible().evaluated: xmin_bup = self.search.xmin - Prim_samples = self.search._candidate_points_set - self.search.xmin = bestInf#B.get_best_infeasible() - vvs, _ = self.search.generate_sample_points(int(((self.search.dim+1)/2)*((self.search.dim+2)/2)) if self.search.ns is None else self.search.ns) - self.search._candidate_points_set += Prim_samples + prim_samples = self.search._candidate_points_set + self.search.xmin = best_inf#B.get_best_infeasible() + self.search.generate_sample_points(int(((self.search.dim+1)/2)*((self.search.dim+2)/2)) if self.search.ns is None else self.search.ns) + self.search._candidate_points_set += prim_samples self.search.xmin = xmin_bup - - if isinstance(vvs, list) and len(vvs) > 0: - vv = vvp + vvs - else: - vv = vvp """ Save current poll directions and incumbent solution @@ -173,36 +159,32 @@ def search_step(self, xmin: SS.CandidatePoint=None): self.search.bb_output = [] xt = [] """ Serial evaluation for points in the poll set """ - if self.search_VN is not None: - self.search.lb = self.search_VN.params.lb - self.search.ub = self.search_VN.params.ub + if self.search_vns is not None: + self.search.lb = self.search_vns.params.lb + self.search.ub = self.search_vns.params.ub self.search.bb_handle.xmin = xmin - self.search.constraints_RP.LAMBDA = xmin.LAMBDA - self.search.constraints_RP.RHO = xmin.RHO + self.search.constraints_RP.LAMBDA = xmin.lambda_multipliers + self.search.constraints_RP.RHO = xmin.rho self.search.constraints_RP.constraints_type = xmin.constraints_type - self.search.constraints_RP.hmax = xmin.hmax + self.search.constraints_RP.hmax = xmin.h_max if not self.options.parallel_mode: - xt, self.post, self.peval = self.search.bb_handle.run_callable_serial_local(iter=self.iteration, peval=self.peval, eval_set=self.search._candidate_points_set, options=self.options, post=self.post, psize=self.search.mesh.getDeltaFrameSize().coordinates, stepName=f'Search: {self.search.type}', mesh=self.search.mesh, constraintsRelaxation=self.search.constraints_RP.__dict__, budget=self.options.budget) + xt, self.post, self.peval = self.search.bb_handle.run_callable_serial_local(iter=self.iteration, peval=self.peval, eval_set=self.search._candidate_points_set, options=self.options, post=self.post, psize=self.search.mesh.getDeltaFrameSize().coordinates, step_name=f'Search: {self.search.type}', mesh=self.search.mesh, constraints_relaxation=self.search.constraints_RP.__dict__, budget=self.options.budget) else: self.search._point_index = -1 """ Parallel evaluation for points in the samples set """ - self.search.bb_eval, xt, self.post, self.peval = self.search.bb_handle.run_callable_parallel_local(iter=self.iteration, peval=self.peval, njobs=self.options.np, eval_set=self.search._candidate_points_set, options=self.options, post=self.post, mesh=self.search.mesh, stepName=f'Search: {self.search.type}', psize=self.search.mesh.getDeltaFrameSize().coordinates, constraintsRelaxation=self.search.constraints_RP.__dict__, budget=self.options.budget) + self.search.bb_eval, xt, self.post, self.peval = self.search.bb_handle.run_callable_parallel_local(iter=self.iteration, peval=self.peval, eval_set=self.search._candidate_points_set, options=self.options, post=self.post, mesh=self.search.mesh, step_name=f'Search: {self.search.type}', psize=self.search.mesh.getDeltaFrameSize().coordinates, constraints_relaxation=self.search.constraints_RP.__dict__, budget=self.options.budget) - if self.search.bb_handle.constraintsRelaxation: - temp:ConstraintsRelaxationParameters = ConstraintsRelaxationParameters(**self.search.bb_handle.constraintsRelaxation) + if self.search.bb_handle.constraints_relaxation: + temp:ConstraintsRelaxationParameters = ConstraintsRelaxationParameters(**self.search.bb_handle.constraints_relaxation) for i in range(len(temp.LAMBDA)): self.search.constraints_RP.LAMBDA[i] = temp.LAMBDA[i] self.search.constraints_RP.RHO = temp.RHO self.search.constraints_RP.constraints_type = temp.constraints_type self.search.constraints_RP.hmax = temp.hmax - # if options.store_cache: - # for xi in xt: - # search.hashtable.hash_id = xi - # if not search.hashtable._isPareto: - # search.hashtable.add_to_best_cache(xi) - self.LAMBDA_k = self.search.bb_handle.constraintsRelaxation["LAMBDA"] - self.RHO_k = self.search.bb_handle.constraintsRelaxation["RHO"] + + self.lambda_multipliers_k = self.search.bb_handle.constraints_relaxation["LAMBDA"] + self.rho_k = self.search.bb_handle.constraints_relaxation["RHO"] self.search.postprocess_evaluated_candidates(xt) @@ -226,14 +208,12 @@ def search_step(self, xmin: SS.CandidatePoint=None): """ Updates """ if self.search.success == SUCCESS_TYPES.FS: - dir: Point = Point(self.search.mesh._n) - dir.coordinates = self.search.xmin.direction.coordinates - # search.mesh.psize = np.multiply(search.mesh.get, 2, dtype=search.dtype.dtype) - self.search.mesh.enlargeDeltaFrameSize(direction=dir) + direction: Point = Point(self.search.mesh._n) + direction.coordinates = self.search.xmin.direction.coordinates + self.search.mesh.enlargeDeltaFrameSize(direction=direction) if self.search.sampling_t != SAMPLING_METHOD.ACTIVE.name: self.search.update_local_region(region="expand") elif self.search.success == SUCCESS_TYPES.US: - # search.mesh.psize = np.divide(search.mesh.psize, 2, dtype=search.dtype.dtype) self.search.mesh.refineDeltaFrameSize() if self.search.sampling_t != SAMPLING_METHOD.ACTIVE.name: self.search.update_local_region(region="contract") @@ -241,33 +221,33 @@ def search_step(self, xmin: SS.CandidatePoint=None): xpost: List[CandidatePoint] = [] for i in range(len(xt)): xpost.append(xt[i]) - updated, updatedF, updatedInf = B.updateWithPoints(evalPointList=xpost, evalType=None, keepAllPoints=False, updateInfeasibleIncumbentAndHmax=True) + updated, updated_f, updated_inf = B.updateWithPoints(eval_point_list=xpost, keep_all_points=False) if not updated: - newMesh = None + new_mesh = None if B._currentIncumbentInf: B._currentIncumbentInf.mesh.refineDeltaFrameSize() - newMesh = copy.deepcopy(B._currentIncumbentFeas.mesh) if B._currentIncumbentFeas else copy.deepcopy(B._currentIncumbentInf.mesh) if B._currentIncumbentInf else None + new_mesh = copy.deepcopy(B._currentIncumbentFeas.mesh) if B._currentIncumbentFeas else copy.deepcopy(B._currentIncumbentInf.mesh) if B._currentIncumbentInf else None B.updateCurrentIncumbents() if self.search.sampling_t != SAMPLING_METHOD.ACTIVE.name: self.search.update_local_region(region="contract") if B._currentIncumbentFeas: B._currentIncumbentFeas.mesh.refineDeltaFrameSize() - newMesh = copy.deepcopy(B._currentIncumbentFeas.mesh) if B._currentIncumbentFeas else copy.deepcopy(B._currentIncumbentInf.mesh) if B._currentIncumbentInf else None + new_mesh = copy.deepcopy(B._currentIncumbentFeas.mesh) if B._currentIncumbentFeas else copy.deepcopy(B._currentIncumbentInf.mesh) if B._currentIncumbentInf else None B.updateCurrentIncumbents() if self.search.sampling_t != SAMPLING_METHOD.ACTIVE.name: self.search.update_local_region(region="contract") if self.iteration == 1: self.search.vicinity_ratio = np.ones((len(self.search.xmin.coordinates),1)) - if newMesh: - self.search.mesh = newMesh + if new_mesh: + self.search.mesh = new_mesh else: self.search.mesh.refineDeltaFrameSize() if self.search.sampling_t != SAMPLING_METHOD.ACTIVE.name: self.search.update_local_region(region="contract") else: - self.search.mesh = copy.deepcopy(B._currentIncumbentFeas.mesh) if updatedF else copy.deepcopy(B._currentIncumbentInf.mesh) if updatedInf else self.search.mesh - self.search.xmin = copy.deepcopy(B._currentIncumbentFeas) if updatedF else copy.deepcopy(B._currentIncumbentInf) if updatedInf else self.search.xmin + self.search.mesh = copy.deepcopy(B._currentIncumbentFeas.mesh) if updated_f else copy.deepcopy(B._currentIncumbentInf.mesh) if updated_inf else self.search.mesh + self.search.xmin = copy.deepcopy(B._currentIncumbentFeas) if updated_f else copy.deepcopy(B._currentIncumbentInf) if updated_inf else self.search.xmin if self.search.sampling_t != SAMPLING_METHOD.ACTIVE.name: self.search.update_local_region(region="expand") @@ -275,7 +255,7 @@ def search_step(self, xmin: SS.CandidatePoint=None): self.post.poll_dirs.append(xpost[i]) self.search.hashtable.best_hash_ID = [] self.search.hashtable.add_to_best_cache(B.getAllPoints()) - self.post.xmin = B._currentIncumbentFeas if updatedF else B._currentIncumbentInf if updatedInf else self.search.xmin + self.post.xmin = B._currentIncumbentFeas if updated_f else B._currentIncumbentInf if updated_inf else self.search.xmin self.search.mesh.update() @@ -290,21 +270,11 @@ def search_step(self, xmin: SS.CandidatePoint=None): self.log.log_msg(f" Run completed in {toc - tic:.4f} seconds", MSG_TYPE.INFO) self.log.log_msg(msg=f" Success status: {self.search.success}", msg_type=MSG_TYPE.INFO) self.log.log_msg(msg=self.post.__str__(), msg_type=MSG_TYPE.INFO) - # log.log_msg(f" Random numbers generator's seed {options.seed}", MSG_TYPE.INFO) - # log.log_msg(" xmin = " + str(search.xmin), MSG_TYPE.INFO) - # log.log_msg(" hmin = " + str(search.xmin.h), MSG_TYPE.INFO) - # log.log_msg(" fmin = " + str(search.xmin.f), MSG_TYPE.INFO) - # log.log_msg(" #bb_eval = " + str(search.bb_handle.bb_eval), MSG_TYPE.INFO) - # log.log_msg(" nb_success = " + str(search.nb_success), MSG_TYPE.INFO) - - # Failure_check = iteration > 0 and search.Failure_stop is not None and search.Failure_stop and not search.success - # if (Failure_check) or (abs(search.mesh.msize) < options.tol or search.bb_eval >= options.budget or search.terminate): - # break - # iteration += 1 - self.RHO_k = self.search.RHO + + self.rho_k = self.search.RHO self.HT = self.search.hashtable self.active_barrier = B - self.LAMBDA_k = self.search.LAMBDA + self.lambda_multipliers_k = self.search.LAMBDA return self.search.xmin def poll_step(self, xmin: PS.CandidatePoint=None): @@ -328,22 +298,21 @@ def poll_step(self, xmin: PS.CandidatePoint=None): B.update_and_reset_success() elif isinstance(B, BarrierMO) and self.iteration == 1: - B.init(evalPointList=[xmin]) + B.init(eval_point_list=[xmin]) if isinstance(B, Barrier): - self.poll.hmax = xmin.hmax + self.poll.hmax = xmin.h_max self.poll.create_poll_set(hhm=hhm, ub=self.param.ub, lb=self.param.lb, it=self.iteration, var_type=xmin.var_type, var_sets=xmin.sets, var_link = xmin.var_link, c_types=self.param.constraints_type, is_prim=True) if B._sec_poll_center is not None and B._sec_poll_center.evaluated: del self.poll.poll_set - # poll.poll_dirs = [] self.poll.x_sc = B._sec_poll_center self.poll.create_poll_set(hhm=hhm, ub=self.param.ub, lb=self.param.lb, it=self.iteration, var_type=B._sec_poll_center.var_type, var_sets=B._sec_poll_center.sets, var_link = B._sec_poll_center.var_link, c_types=self.param.constraints_type, is_prim=False) elif isinstance(B, BarrierMO): - self.poll.hmax = B._hMax + self.poll.hmax = B._h_max del self.poll.poll_set del self.poll.poll_dirs if B._currentIncumbentFeas and B._currentIncumbentFeas.evaluated: @@ -356,7 +325,6 @@ def poll_step(self, xmin: PS.CandidatePoint=None): lb=self.param.lb, it=self.iteration, var_type=self.poll.xmin.var_type, var_sets=self.poll.xmin.sets, var_link = self.poll.xmin.var_link, c_types=self.param.constraints_type, is_prim=True) if B._currentIncumbentInf and B._currentIncumbentInf.evaluated: - # del poll.poll_set self.poll.x_sc = B._currentIncumbentInf self.poll.create_poll_set(hhm=hhm, ub=self.param.ub, @@ -367,8 +335,8 @@ def poll_step(self, xmin: PS.CandidatePoint=None): lb=self.param.lb, it=self.iteration, var_type=self.poll.xmin.var_type, var_sets=self.poll.xmin.sets, var_link = self.poll.xmin.var_link, c_types=self.param.constraints_type, is_prim=False) - self.poll.LAMBDA = self.LAMBDA_k - self.poll.RHO = self.RHO_k + self.poll.LAMBDA = self.lambda_multipliers_k + self.poll.RHO = self.rho_k """ Save current poll directions and incumbent solution so they can be saved later in the post dir """ @@ -383,37 +351,32 @@ def poll_step(self, xmin: PS.CandidatePoint=None): xt = [] self.poll.bb_handle.xmin = xmin """ Serial evaluation for points in the poll set """ - self.poll.constraints_RP.LAMBDA = xmin.LAMBDA - self.poll.constraints_RP.RHO = xmin.RHO + self.poll.constraints_RP.LAMBDA = xmin.lambda_multipliers + self.poll.constraints_RP.RHO = xmin.rho self.poll.constraints_RP.constraints_type = xmin.constraints_type - self.poll.constraints_RP.hmax = xmin.hmax + self.poll.constraints_RP.hmax = xmin.h_max if not self.options.parallel_mode: - xt, self.post, self.peval = self.poll.bb_handle.run_callable_serial_local(iter=self.iteration, peval=self.peval, eval_set=self.poll._candidate_points_set, options=self.options, post=self.post, psize=self.poll.mesh.getDeltaFrameSize().coordinates, stepName=f'Poll Step', mesh=self.poll.mesh, constraintsRelaxation=self.poll.constraints_RP.__dict__, budget=self.options.budget) + xt, self.post, self.peval = self.poll.bb_handle.run_callable_serial_local(iter=self.iteration, peval=self.peval, eval_set=self.poll._candidate_points_set, options=self.options, post=self.post, psize=self.poll.mesh.getDeltaFrameSize().coordinates, step_name='Poll Step', mesh=self.poll.mesh, constraints_relaxation=self.poll.constraints_RP.__dict__, budget=self.options.budget) else: self.poll.point_index = -1 """ Parallel evaluation for points in the samples set """ - self.poll.bb_eval, xt, self.post, self.peval = self.poll.bb_handle.run_callable_parallel_local(iter=self.iteration, peval=self.peval, njobs=self.options.np, eval_set=self.poll._candidate_points_set, options=self.options, post=self.post, mesh=self.poll.mesh, stepName=f'Poll Step', psize=self.poll.mesh.getDeltaFrameSize().coordinates, constraintsRelaxation=self.poll.constraints_RP.__dict__, budget=self.options.budget) + self.poll.bb_eval, xt, self.post, self.peval = self.poll.bb_handle.run_callable_parallel_local(iter=self.iteration, peval=self.peval, eval_set=self.poll._candidate_points_set, options=self.options, post=self.post, mesh=self.poll.mesh, step_name='Poll Step', psize=self.poll.mesh.getDeltaFrameSize().coordinates, constraints_relaxation=self.poll.constraints_RP.__dict__, budget=self.options.budget) - if self.poll.bb_handle.constraintsRelaxation: - temp:ConstraintsRelaxationParameters = ConstraintsRelaxationParameters(**self.poll.bb_handle.constraintsRelaxation) + if self.poll.bb_handle.constraints_relaxation: + temp:ConstraintsRelaxationParameters = ConstraintsRelaxationParameters(**self.poll.bb_handle.constraints_relaxation) for i in range(len(temp.LAMBDA)): self.poll.constraints_RP.LAMBDA[i] = temp.LAMBDA[i] self.poll.constraints_RP.RHO = temp.RHO self.poll.constraints_RP.constraints_type = temp.constraints_type self.poll.constraints_RP.hmax = temp.hmax - # if options.store_cache: - # for xi in xt: - # poll.hashtable.hash_id = xi - # if not poll.hashtable._isPareto: - # poll.hashtable.add_to_best_cache(xi) - self.LAMBDA_k = self.poll.bb_handle.constraintsRelaxation["LAMBDA"] - self.RHO_k = self.poll.bb_handle.constraintsRelaxation["RHO"] + + self.lambda_multipliers_k = self.poll.bb_handle.constraints_relaxation["LAMBDA"] + self.rho_k = self.poll.bb_handle.constraints_relaxation["RHO"] self.poll.postprocess_evaluated_candidates(xt) if isinstance(B, Barrier): xpost: List[CandidatePoint] = self.poll.master_updates(xt, self.peval, save_all_best=self.options.save_all_best, save_all=self.options.save_results) - xmin = copy.deepcopy(self.poll.xmin) if self.options.save_results: for i in range(len(xpost)): self.post.poll_dirs.append(xpost[i]) @@ -429,37 +392,35 @@ def poll_step(self, xmin: PS.CandidatePoint=None): for p in self.poll.poll_set: if p.evaluated: pev += 1 - # if pev != poll.poll_dirs and not poll.success: - # poll.seed += 1 - goToSearch: bool = (pev == 0 and self.poll.Failure_stop is not None and self.poll.Failure_stop) + + go_to_search: bool = (pev == 0 and self.poll.Failure_stop is not None and self.poll.Failure_stop) - dir: Point = Point(self.poll._n) - dir.coordinates = self.poll.xmin.direction.coordinates if self.poll.xmin.direction is not None else [0]*self.poll._n - if self.poll.success == SUCCESS_TYPES.FS and not goToSearch: - self.poll.mesh.enlargeDeltaFrameSize(direction=dir) # poll.mesh.psize = np.multiply(poll.mesh.psize, 2, dtype=poll.dtype.dtype + direction: Point = Point(self.poll._n) + direction.coordinates = self.poll.xmin.direction.coordinates if self.poll.xmin.direction is not None else [0]*self.poll._n + if self.poll.success == SUCCESS_TYPES.FS and not go_to_search: + self.poll.mesh.enlargeDeltaFrameSize(direction=direction) # poll.mesh.psize = np.multiply(poll.mesh.psize, 2, dtype=poll.dtype.dtype elif self.poll.success == SUCCESS_TYPES.US: self.poll.mesh.refineDeltaFrameSize() - # poll.mesh.psize = np.divide(poll.mesh.psize, 2, dtype=poll.dtype.dtype) elif isinstance(B, BarrierMO): xpost: List[CandidatePoint] = [] for i in range(len(xt)): xpost.append(xt[i]) - updated, _, _ = B.updateWithPoints(evalPointList=xpost, evalType=None, keepAllPoints=False, updateInfeasibleIncumbentAndHmax=True) + updated, _, _ = B.updateWithPoints(eval_point_list=xpost, keep_all_points=False) if not updated: - newMesh = None + new_mesh = None if B._currentIncumbentInf: B._currentIncumbentInf.mesh.refineDeltaFrameSize() - newMesh = copy.deepcopy(B._currentIncumbentFeas.mesh) if B._currentIncumbentFeas else copy.deepcopy(B._currentIncumbentInf.mesh) if B._currentIncumbentInf else None + new_mesh = copy.deepcopy(B._currentIncumbentFeas.mesh) if B._currentIncumbentFeas else copy.deepcopy(B._currentIncumbentInf.mesh) if B._currentIncumbentInf else None B.updateCurrentIncumbents() if B._currentIncumbentFeas: B._currentIncumbentFeas.mesh.refineDeltaFrameSize() - newMesh = copy.deepcopy(B._currentIncumbentFeas.mesh) if B._currentIncumbentFeas else copy.deepcopy(B._currentIncumbentInf.mesh) if B._currentIncumbentInf else None + new_mesh = copy.deepcopy(B._currentIncumbentFeas.mesh) if B._currentIncumbentFeas else copy.deepcopy(B._currentIncumbentInf.mesh) if B._currentIncumbentInf else None B.updateCurrentIncumbents() - if newMesh: - self.poll.mesh = newMesh + if new_mesh: + self.poll.mesh = new_mesh else: self.poll.mesh.refineDeltaFrameSize() @@ -476,8 +437,8 @@ def poll_step(self, xmin: PS.CandidatePoint=None): if self.options.display: print(self.post) - self.LAMBDA_k = self.poll.LAMBDA - self.RHO_k = self.poll.xmin.RHO + self.lambda_multipliers_k = self.poll.LAMBDA + self.rho_k = self.poll.xmin.rho toc = time.perf_counter() @@ -486,19 +447,10 @@ def poll_step(self, xmin: PS.CandidatePoint=None): self.log.log_msg(msg=f" Run completed in {toc - tic:.4f} seconds", msg_type=MSG_TYPE.INFO) self.log.log_msg(msg=f" Success status: {self.poll.success}", msg_type=MSG_TYPE.INFO) self.log.log_msg(msg=self.post.__str__(), msg_type=MSG_TYPE.INFO) - # log.log_msg(msg=f" Random numbers generator's seed {options.seed}", msg_type=MSG_TYPE.INFO) - # log.log_msg(msg=f" xmin = {poll.xmin.__str__()} ", msg_type=MSG_TYPE.INFO) - # log.log_msg(msg=f" hmin = {poll.xmin.h} ", msg_type=MSG_TYPE.INFO) - # log.log_msg(msg=f" fmin {poll.xmin.fobj}", msg_type=MSG_TYPE.INFO) - # log.log_msg(msg=f" #bb_eval = {poll.bb_eval} ", msg_type=MSG_TYPE.INFO) - # log.log_msg(msg=f" #iteration = {iteration} ", msg_type=MSG_TYPE.INFO) - # log.log_msg(msg=f" nb_success = {poll.nb_success} ", msg_type=MSG_TYPE.INFO) - # log.log_msg(msg=f" psize = {poll.mesh.psize} ", msg_type=MSG_TYPE.INFO) - # log.log_msg(msg=f" psize_success = {poll.mesh.psize_success} ", msg_type=MSG_TYPE.INFO) - # log.log_msg(msg=f" psize_max = {poll.mesh.psize_max} ", msg_type=MSG_TYPE.INFO) + self.HT = self.poll.hashtable self.active_barrier = B - self.LAMBDA_k = self.poll.xmin.LAMBDA + self.lambda_multipliers_k = self.poll.xmin.lambda_multipliers return self.poll.xmin def main(*args) -> Dict[str, Any]: @@ -532,7 +484,7 @@ def main(*args) -> Dict[str, Any]: if not os.path.exists(data["param"]["post_dir"]): try: os.mkdir(data["param"]["post_dir"]) - except: + except Warning: os.makedirs(data["param"]["post_dir"], exist_ok=True) log.initialize(data["param"]["post_dir"] + "/OMADS.log") @@ -540,7 +492,7 @@ def main(*args) -> Dict[str, Any]: """ Run preprocessor for the setup of the optimization problem and for the initialization of optimization process """ - MADS_agent: MADS = MADS(data=data) + mads_agent: MADS = MADS(data=data) iteration: int xmin: CandidatePoint options: Options @@ -551,16 +503,14 @@ def main(*args) -> Dict[str, Any]: # poll: PS.Dirs2n search: SS.efficient_exploration log.log_msg(msg="Preprocess the search step...", msg_type=PS.MSG_TYPE.INFO) - _, _, MADS_agent.search, _, _, _, _, _, _ = SS.PreExploration(data).initialize_from_dict(log=log) + _, _, mads_agent.search, _, _, _, _, _, _ = SS.PreExploration(data).initialize_from_dict(log=log) log.log_msg(msg="Preprocess the MADS algorithim...", msg_type=PS.MSG_TYPE.INFO) - iteration, xmin, MADS_agent.poll, options, param, post, out, B, outP = PS.PrePoll(data).initialize_from_dict(log=log, xs=MADS_agent.search.xmin) - out.stepName = "Poll" - post.step_name = [f'Search: {MADS_agent.search.type}'] + iteration, xmin, mads_agent.poll, options, param, post, out, B, out_p = PS.PrePoll(data).initialize_from_dict(log=log, xs=mads_agent.search.xmin) + out.step_name = "Poll" + post.step_name = [f'Search: {mads_agent.search.type}'] - HT = MADS_agent.poll.hashtable + HT = mads_agent.poll.hashtable - # if MADS_LINK.REPLACE is not None and not MADS_LINK.REPLACE: - # out.replace = False """ Set the random seed for results reproducibility """ if len(args) < 4: @@ -571,76 +521,80 @@ def main(*args) -> Dict[str, Any]: """ Start the count down for calculating the runtime indicator """ tic = PS.time.perf_counter() peval = 0 - LAMBDA_k = xmin.LAMBDA - RHO_k = xmin.RHO + lambda_multipliers = xmin.lambda_multipliers + rho_k = xmin.rho - if MADS_agent.search.type == SS.SEARCH_TYPE.VNS.name: - search_VN = SS.VNS(active_barrier=B, params=param) - search_VN._ns_dist = [int(((MADS_agent.search.dim+1)/2)*((MADS_agent.search.dim+2)/2)/(len(search_VN._dist))) if MADS_agent.search.ns is None else MADS_agent.search.ns] * len(search_VN._dist) - MADS_agent.search.ns = sum(search_VN._ns_dist) + if mads_agent.search.type == SS.SEARCH_TYPE.VNS.name: + search_vn = SS.VNS(active_barrier=B, params=param) + search_vn._ns_dist = [int(((mads_agent.search.dim+1)/2)*((mads_agent.search.dim+2)/2)/(len(search_vn._dist))) if mads_agent.search.ns is None else mads_agent.search.ns] * len(search_vn._dist) + mads_agent.search.ns = sum(search_vn._ns_dist) else: - search_VN = None + search_vn = None - MADS_agent.search.lb = param.lb - MADS_agent.search.ub = param.ub - MADS_agent.options = options - MADS_agent.param = param - MADS_agent.log = log - MADS_agent.outP = outP - MADS_agent.out = out - MADS_agent.post = post - MADS_agent.HT = HT - MADS_agent.peval = peval - MADS_agent.RHO_k = RHO_k - MADS_agent.active_barrier = B - MADS_agent.LAMBDA_k = LAMBDA_k - + mads_agent.search.lb = param.lb + mads_agent.search.ub = param.ub + mads_agent.options = options + mads_agent.param = param + mads_agent.log = log + mads_agent.out_p = out_p + mads_agent.out = out + mads_agent.post = post + mads_agent.HT = HT + mads_agent.peval = peval + mads_agent.rho_k = rho_k + mads_agent.active_barrier = B + mads_agent.lambda_multipliers_k = lambda_multipliers + original_st = copy.deepcopy(mads_agent.search.sampling_t) while True: """ Run search step (Optional) """ - # TODO: This rule cannot be generalized -- needs further invistigation + # COMPLETED: This rule cannot be generalized -- needs further invistigation # if poll.dim > 10 and poll.mesh.psize >= 1E-4: # canSearch = False # else: - canSearch = True - MADS_agent.iteration = iteration + can_search = True + mads_agent.iteration = iteration - if canSearch and (MADS_agent.poll.success == SUCCESS_TYPES.US or iteration == 1): - MADS_agent.log.log_msg(f"------- Iteration # {iteration}: Run the search step -------", MSG_TYPE.INFO) - MADS_agent.search.iter = iteration - xmin = MADS_agent.search_step(xmin=xmin) + if can_search and (mads_agent.poll.success == SUCCESS_TYPES.US or iteration == 1): + mads_agent.log.log_msg(f"------- Iteration # {iteration}: Run the search step -------", MSG_TYPE.INFO) + mads_agent.search.iter = iteration + xmin = mads_agent.search_step(xmin=xmin) """ Run the poll step (Mandatory step) """ - MADS_agent.log.log_msg(f"------- Iteration # {iteration}: Run the poll step -------", MSG_TYPE.INFO) - xmin = MADS_agent.poll_step(xmin=xmin) - xmin = MADS_agent.poll.xmin - MADS_agent.search.mesh = copy.deepcopy(MADS_agent.poll.mesh) - MADS_agent.search.psize = copy.deepcopy(MADS_agent.poll.psize) + mads_agent.log.log_msg(f"------- Iteration # {iteration}: Run the poll step -------", MSG_TYPE.INFO) + xmin = mads_agent.poll_step(xmin=xmin) + xmin = mads_agent.poll.xmin + mads_agent.search.mesh = copy.deepcopy(mads_agent.poll.mesh) + mads_agent.search.psize = copy.deepcopy(mads_agent.poll.psize) """ Check stopping criteria""" - pt = (all(abs(MADS_agent.poll.mesh.getDeltaFrameSize().coordinates[pp]) < options.tol for pp in range(MADS_agent.poll._n))) - st = (all(abs(MADS_agent.search.mesh.getdeltaMeshSize().coordinates[pp]) < options.tol for pp in range(MADS_agent.search.mesh._n))) + pt = (all(abs(mads_agent.poll.mesh.getDeltaFrameSize().coordinates[pp]) < options.tol for pp in range(mads_agent.poll._n))) + st = (all(abs(mads_agent.search.mesh.getdeltaMeshSize().coordinates[pp]) < options.tol for pp in range(mads_agent.search.mesh._n))) if options.save_results: - MADS_agent.post.output_results(out, False) + mads_agent.post.output_results(out, False) if param.isPareto: - MADS_agent.post.nd_points = [] + mads_agent.post.nd_points = [] for i in range(len(B.getAllPoints())): - MADS_agent.post.nd_points.append(B.getAllPoints()[i]) - MADS_agent.post.output_nd_results(outP) - if (pt or st or MADS_agent.search.bb_eval + MADS_agent.poll.bb_eval >= options.budget): - MADS_agent.log.log_msg(f"\n--------------- Termination of MADS ---------------", MSG_TYPE.INFO) + mads_agent.post.nd_points.append(B.getAllPoints()[i]) + mads_agent.post.output_nd_results(out_p) + if (pt or st or mads_agent.search.bb_eval + mads_agent.poll.bb_eval >= options.budget): + mads_agent.log.log_msg("\n--------------- Termination of MADS ---------------", MSG_TYPE.INFO) if pt: - MADS_agent.log.log_msg(f"Termination criterion hit: the poll size is below the minimum threshold defined.", MSG_TYPE.INFO) + mads_agent.log.log_msg("Termination criterion hit: the poll size is below the minimum threshold defined.", MSG_TYPE.INFO) if st: - MADS_agent.log.log_msg(f"Termination criterion hit: the mesh size is below the minimum threshold defined.", MSG_TYPE.INFO) - if (MADS_agent.search.bb_eval + MADS_agent.poll.bb_eval >= options.budget): - MADS_agent.log.log_msg(f"Termination criterion hit: Evaluation budget is exhausted.", MSG_TYPE.INFO) - MADS_agent.log.log_msg(f"----------------------------------------------------\n", MSG_TYPE.INFO) + mads_agent.log.log_msg("Termination criterion hit: the mesh size is below the minimum threshold defined.", MSG_TYPE.INFO) + if (mads_agent.search.bb_eval + mads_agent.poll.bb_eval >= options.budget): + mads_agent.log.log_msg("Termination criterion hit: Evaluation budget is exhausted.", MSG_TYPE.INFO) + mads_agent.log.log_msg("----------------------------------------------------\n", MSG_TYPE.INFO) break iteration += 1 toc = PS.time.perf_counter() if isinstance(B, BarrierMO): - perfM = Metrics(ND_solutions=B.getAllPoints(), nobj=B._nobj) - HV = perfM.hypervolume() + rp: Optional[CandidatePoint] = None + if param.ref_point: + rp = Point() + rp.coordinates = param.ref_point + perf_m = Metrics(nd_solutions=B.getAllPoints(), nobj=B._nobj, ref_point=rp) + HV = perf_m.hypervolume() """ If benchmarking, then populate the results in the benchmarking output report """ if importlib.util.find_spec('BMDFO') and len(args) > 1 and isinstance(args[1], PS.toy.Run): @@ -649,36 +603,34 @@ def main(*args) -> Dict[str, Any]: ncon = 0 else: ncon = len(xmin.c_ineq) - if len(MADS_agent.poll.bb_output) > 0: - b.add_row(name=MADS_agent.poll.bb_handle.blackbox, + if len(mads_agent.poll.bb_output) > 0: + b.add_row(name=mads_agent.poll.bb_handle.blackbox, run_index=int(args[2]), nv=len(param.baseline), nc=ncon, - nb_success=MADS_agent.poll.nb_success, + nb_success=mads_agent.poll.nb_success, it=iteration, - BBEVAL=MADS_agent.poll.bb_eval, + BBEVAL=mads_agent.poll.bb_eval, runtime=toc - tic, - feval=MADS_agent.poll.bb_handle.bb_eval, - hmin=MADS_agent.poll.xmin.h, - fmin=MADS_agent.poll.xmin.f) - print(f"{MADS_agent.poll.bb_handle.blackbox}: fmin = {MADS_agent.poll.xmin.f} , hmin= {MADS_agent.poll.xmin.h:.2f}") + feval=mads_agent.poll.bb_handle.bb_eval, + hmin=mads_agent.poll.xmin.h, + fmin=mads_agent.poll.xmin.f) + print(f"{mads_agent.poll.bb_handle.blackbox}: fmin = {mads_agent.poll.xmin.f} , hmin= {mads_agent.poll.xmin.h:.2f}") elif importlib.util.find_spec('BMDFO') and len(args) > 1 and not isinstance(args[1], toy.Run): raise IOError("Could not find " + args[1] + " in the internal BM suite.") - # if options.save_results: - # post.output_results(out) out_step: Any = None - if MADS_agent.poll.xmin < MADS_agent.search.xmin: - out_step = MADS_agent.poll - elif MADS_agent.search.xmin < MADS_agent.poll.xmin: - out_step = MADS_agent.search + if mads_agent.poll.xmin < mads_agent.search.xmin: + out_step = mads_agent.poll + elif mads_agent.search.xmin < mads_agent.poll.xmin: + out_step = mads_agent.search else: - out_step = MADS_agent.poll + out_step = mads_agent.poll if out_step is None: - out_step = MADS_agent.poll + out_step = mads_agent.poll if options.display: @@ -686,27 +638,26 @@ def main(*args) -> Dict[str, Any]: print(" Final objective value: " + str(out_step.xmin.f) + ", hmin= " + str(out_step.xmin.h)) if options.save_coordinates: - MADS_agent.post.output_coordinates(out) + mads_agent.post.output_coordinates(out) - if MADS_agent.log is not None: - MADS_agent.log.log_msg(msg=" --- MADS Run Summary--- ", msg_type=MSG_TYPE.INFO) - MADS_agent.log.log_msg(msg=f" Run completed in {toc - tic:.4f} seconds", msg_type=MSG_TYPE.INFO) - MADS_agent.log.log_msg(msg=f" # of successful search steps = {MADS_agent.search.n_successes}", msg_type=MSG_TYPE.INFO) - MADS_agent.log.log_msg(msg=f" # of successful poll steps = {MADS_agent.poll.n_successes}", msg_type=MSG_TYPE.INFO) - MADS_agent.log.log_msg(msg=f" Run completed in {toc - tic:.4f} seconds", msg_type=MSG_TYPE.INFO) - MADS_agent.log.log_msg(msg=f" Random numbers generator's seed {options.seed}", msg_type=MSG_TYPE.INFO) - MADS_agent.log.log_msg(msg=f" xmin = {MADS_agent.poll.xmin.__str__()} ", msg_type=MSG_TYPE.INFO) - MADS_agent.log.log_msg(msg=f" hmin = {MADS_agent.poll.xmin.h} ", msg_type=MSG_TYPE.INFO) - MADS_agent.log.log_msg(msg=f" fmin {MADS_agent.poll.xmin.fobj}", msg_type=MSG_TYPE.INFO) - MADS_agent.log.log_msg(msg=f" Search step # BB evals = {MADS_agent.search.bb_eval} ", msg_type=MSG_TYPE.INFO) - MADS_agent.log.log_msg(msg=f" Poll step # BB evals = {MADS_agent.poll.bb_eval} ", msg_type=MSG_TYPE.INFO) - MADS_agent.log.log_msg(msg=f" Total # BB evals = {MADS_agent.poll.bb_eval + MADS_agent.search.bb_eval} ", msg_type=MSG_TYPE.INFO) - MADS_agent.log.log_msg(msg=f" #iterations = {iteration} ", msg_type=MSG_TYPE.INFO) - MADS_agent.log.log_msg(msg=f" psize = {MADS_agent.poll.mesh.getDeltaFrameSize().coordinates} ", msg_type=MSG_TYPE.INFO) - MADS_agent.log.log_msg(msg=f" psize_success = {MADS_agent.poll.xmin.mesh.getDeltaFrameSize().coordinates}", msg_type=MSG_TYPE.INFO) + if mads_agent.log is not None: + mads_agent.log.log_msg(msg=" --- MADS Run Summary--- ", msg_type=MSG_TYPE.INFO) + mads_agent.log.log_msg(msg=f" Run completed in {toc - tic:.4f} seconds", msg_type=MSG_TYPE.INFO) + mads_agent.log.log_msg(msg=f" # of successful search steps = {mads_agent.search.n_successes}", msg_type=MSG_TYPE.INFO) + mads_agent.log.log_msg(msg=f" # of successful poll steps = {mads_agent.poll.n_successes}", msg_type=MSG_TYPE.INFO) + mads_agent.log.log_msg(msg=f" Run completed in {toc - tic:.4f} seconds", msg_type=MSG_TYPE.INFO) + mads_agent.log.log_msg(msg=f" Random numbers generator's seed {options.seed}", msg_type=MSG_TYPE.INFO) + mads_agent.log.log_msg(msg=f" xmin = {mads_agent.poll.xmin.__str__()} ", msg_type=MSG_TYPE.INFO) + mads_agent.log.log_msg(msg=f" hmin = {mads_agent.poll.xmin.h} ", msg_type=MSG_TYPE.INFO) + mads_agent.log.log_msg(msg=f" fmin {mads_agent.poll.xmin.fobj}", msg_type=MSG_TYPE.INFO) + mads_agent.log.log_msg(msg=f" Search step # BB evals = {mads_agent.search.bb_eval} ", msg_type=MSG_TYPE.INFO) + mads_agent.log.log_msg(msg=f" Poll step # BB evals = {mads_agent.poll.bb_eval} ", msg_type=MSG_TYPE.INFO) + mads_agent.log.log_msg(msg=f" Total # BB evals = {mads_agent.poll.bb_eval + mads_agent.search.bb_eval} ", msg_type=MSG_TYPE.INFO) + mads_agent.log.log_msg(msg=f" #iterations = {iteration} ", msg_type=MSG_TYPE.INFO) + mads_agent.log.log_msg(msg=f" psize = {mads_agent.poll.mesh.getDeltaFrameSize().coordinates} ", msg_type=MSG_TYPE.INFO) + mads_agent.log.log_msg(msg=f" psize_success = {mads_agent.poll.xmin.mesh.getDeltaFrameSize().coordinates}", msg_type=MSG_TYPE.INFO) if isinstance(B, BarrierMO): - MADS_agent.log.log_msg(msg=f" Hypervolume metric = {HV}", msg_type=MSG_TYPE.INFO) - # log.log_msg(msg=f" psize_max = {poll.mesh.psize_max} ", msg_type=MSG_TYPE.INFO) + mads_agent.log.log_msg(msg=f" Hypervolume metric = {HV}", msg_type=MSG_TYPE.INFO) if options.display: print("\n ---MADS Run Summary---") print(f" Run completed in {toc - tic:.4f} seconds") @@ -716,10 +667,9 @@ def main(*args) -> Dict[str, Any]: print(" fmin = " + str(out_step.xmin.f)) print(" #bb_eval = " + str(out_step.bb_eval)) print(" #iteration = " + str(iteration)) - print(" nb_success = " + str(MADS_agent.poll.nb_success + MADS_agent.search.nb_success)) - print(" psize = " + str(MADS_agent.poll.mesh.getDeltaFrameSize().coordinates)) - print(" psize_success = " + str(MADS_agent.poll.xmin.mesh.getDeltaFrameSize().coordinates)) - # print(" psize_max = " + str(poll.mesh.psize_max)) + print(" nb_success = " + str(mads_agent.poll.nb_success + mads_agent.search.nb_success)) + print(" psize = " + str(mads_agent.poll.mesh.getDeltaFrameSize().coordinates)) + print(" psize_success = " + str(mads_agent.poll.xmin.mesh.getDeltaFrameSize().coordinates)) xmin = out_step.xmin """ Evaluation of the blackbox; get output responses """ @@ -737,11 +687,12 @@ def main(*args) -> Dict[str, Any]: "hmin": out_step.xmin.h, "nbb_evals" : out_step.bb_eval, "niterations" : iteration, - "nb_success": MADS_agent.poll.nb_success + MADS_agent.search.nb_success, - "psize": MADS_agent.poll.mesh.getDeltaFrameSize().coordinates, - "psuccess": MADS_agent.poll.xmin.mesh.getDeltaFrameSize().coordinates, + "nb_success": mads_agent.poll.nb_success + mads_agent.search.nb_success, + "psize": mads_agent.poll.mesh.getDeltaFrameSize().coordinates, + "psuccess": mads_agent.poll.xmin.mesh.getDeltaFrameSize().coordinates, # "pmax": poll.mesh.psize_max, - "msize": out_step.mesh.getdeltaMeshSize().coordinates} + "msize": out_step.mesh.getdeltaMeshSize().coordinates, + "HV": HV if param.isPareto else "NA"} return output, out_step @@ -754,7 +705,7 @@ def rosen(x, *argv): def test_omads_callable_quick(): - eval = {"blackbox": rosen} + eval_bb = {"blackbox": rosen} param = {"baseline": [-2.0, -2.0], "lb": [-5, -5], "ub": [10, 10], @@ -769,7 +720,7 @@ def test_omads_callable_quick(): } options = {"seed": 0, "budget": 100000, "tol": 1e-12, "display": True, "check_cache": True, "store_cache": True, "rich_direction": True, "psize_init": 1., "precision": "high"} - data = {"evaluator": eval, "param": param, "options": options, "sampling": sampling} + data = {"evaluator": eval_bb, "param": param, "options": options, "sampling": sampling} out: Dict = main(data) print(out) diff --git a/src/OMADS/Mesh.py b/src/OMADS/Mesh.py index de005d0..d12efb6 100644 --- a/src/OMADS/Mesh.py +++ b/src/OMADS/Mesh.py @@ -1,6 +1,6 @@ -import copy -from typing import Protocol, Any, List -from ._globals import * +from dataclasses import dataclass +from typing import Protocol, Any, List, Optional +from ._globals import DType, M_INF_INT, P_INF_INT from .Point import Point from .Parameters import Parameters @@ -18,27 +18,24 @@ class MeshData(Protocol): :param _dtype: numpy double data type precision """ - _n: int = None - # _anisotropy_factor: int = 0.1 - # _meshSize: Point = None # mesh size - # _frameSize: Point = None # poll size - _initialMeshSize: Point = None # mesh size - _initialFrameSize: Point = None # poll size - _minMeshSize: Point = None # mesh size - _minFrameSize: Point = None # poll size - _lowerBound: Point = None - _upperBound: Point = None - _isFinest: bool = True - _r: Point = None - _rMin: Point = None - _rMax: Point = None + _n: Optional[int] = None + _initialMeshSize: Optional[Point] = None # mesh size + _initialFrameSize: Optional[Point] = None # poll size + _minMeshSize: Optional[Point] = None # mesh size + _minFrameSize: Optional[Point] = None # poll size + _lowerBound: Optional[Point] = None + _upperBound: Optional[Point] = None + _isFinest: Optional[bool] = True + _r: Optional[Point] = None + _rMin: Optional[Point] = None + _rMax: Optional[Point] = None _limitMinMeshIndex: int = M_INF_INT _limitMaxMeshIndex: int = P_INF_INT - _pbParams: Parameters = None - _rho: List[float] = None # poll size to mesh size ratio - _dtype: DType = None + _pbParams: Optional[Parameters] = None + _rho: Optional[List[float] ]= None # poll size to mesh size ratio + _dtype: Optional[DType] = None - # TODO: manage the poll size granularity for discrete variables + # COMPLETED: manage the poll size granularity for discrete variables # # See: Audet et. al, The mesh adaptive direct search algorithm for # # granular and discrete variable # _exp: int = 0 @@ -69,19 +66,19 @@ def getdeltaMeshSize(self): def getDeltaFrameSize(self, i: int): ... - def getDeltaFrameSizeCoarser(self, i: int): + def getDeltaFrameSizeCoarser(self): ... - def setDeltas(self, i: int = None, deltaMeshSize: Any = None, deltaFrameSize: Any = None): + def setDeltas(self, i: int = None, delta_mesh_size: Any = None, delta_frame_size: Any = None): ... - def scaleAndProjectOnMesh(self, i: int = None, l: float = None, dir: Point = None): + def scaleAndProjectOnMesh(self, dir: Point = None): ... - def projectOnMesh(self, point: Point, frameCenter: Point): + def projectOnMesh(self, point: Point, frame_center: Point): ... - def verifyPointIsOnMesh(self, point: Point, frameCenter: Point): + def verifyPointIsOnMesh(self, point: Point, frame_center: Point): ... def verifyDimension(self, name: str, dim: int): @@ -91,23 +88,23 @@ def verifyDimension(self, name: str, dim: int): @dataclass class Mesh(MeshData): - def __init__(self, pbParams: Parameters, limitMinMeshIndex: int, limitMaxMeshIndex: int): - self._n = pbParams._n - self._initialMeshSize = pbParams.initialMeshSize - self._minMeshSize = pbParams.minMeshSize - self._initialFrameSize = pbParams.initialFrameSize - self._minFrameSize = pbParams.minFrameSize - self._lowerBound = Point(self._n, pbParams.lb) - self._upperBound = Point(self._n, pbParams.ub) + def __init__(self, pb_params: Parameters, limit_min_mesh_index: int, limit_max_mesh_index: int): + self._n = pb_params._n + self._initialMeshSize = pb_params.initialMeshSize + self._minMeshSize = pb_params.minMeshSize + self._initialFrameSize = pb_params.initialFrameSize + self._minFrameSize = pb_params.minFrameSize + self._lowerBound = Point(self._n, pb_params.lb) + self._upperBound = Point(self._n, pb_params.ub) self._isFinest = True self._r = Point(self._n).reset(n=self._n, d=0.) self._rMin = Point(self._n).reset(n=self._n, d=0.) self._rMax = Point(self._n).reset(n=self._n, d=0.) - self._limitMinMeshIndex = limitMinMeshIndex - self._limitMaxMeshIndex = limitMaxMeshIndex + self._limitMinMeshIndex = limit_min_mesh_index + self._limitMaxMeshIndex = limit_max_mesh_index self._dtype = DType() - self._pbParams = pbParams - if (not self._pbParams.toBeChecked()): + self._pbParams = pb_params + if (not self._pbParams.to_be_checked()): raise IOError("Parameters::checkAndComply() needs to be called before constructing a mesh.") @property @@ -142,9 +139,9 @@ def setMeshIndex(self, r:Point): def isFinest(self): return self._isFinest - def setLimitMeshIndices(self, limitMinMeshIndex: int, limitMaxMeshIndex: int): - self._limitMaxMeshIndex = limitMaxMeshIndex - self._limitMinMeshIndex = limitMinMeshIndex + def setLimitMeshIndices(self, limit_min_mesh_index: int, limit_max_mesh_index: int): + self._limitMaxMeshIndex = limit_max_mesh_index + self._limitMinMeshIndex = limit_min_mesh_index diff --git a/src/OMADS/Metrics.py b/src/OMADS/Metrics.py index d802c4f..d5cb999 100644 --- a/src/OMADS/Metrics.py +++ b/src/OMADS/Metrics.py @@ -1,27 +1,69 @@ import copy -from ._globals import * +from dataclasses import dataclass + +import numpy as np from .CandidatePoint import CandidatePoint -from typing import List +from typing import List, Optional +from deap.tools._hypervolume import pyhv as hv +from .Point import Point @dataclass class Metrics: - ND_solutions: List[CandidatePoint] = None + nd_solutions: Optional[List[CandidatePoint]] = None nobj: int = 2 - _ref_point: CandidatePoint = None + ref_point: Optional[Point] = None - def find_ref_point(self): - if self.ND_solutions: - self._nobj = len(self.ND_solutions[0].f) - self._ref_point = CandidatePoint(self.ND_solutions[0].n_dimensions) + if self.nd_solutions: + self._nobj = len(self.nd_solutions[0].f) + self.ref_point = Point() ftemp = [] for i in range(self._nobj): f: List[float] = [] - for p in self.ND_solutions: - f.append(p.f[i]) - ftemp.append(max(f)+1) - self._ref_point.f = copy.deepcopy(ftemp) + for p in self.nd_solutions: + f.append(p.fobj[i]) + ftemp.append(max(f)+abs(max(f))*0.025) + self.ref_point.coordinates = copy.deepcopy(ftemp) + + def get_pareto_points(self): + ftemp = [] + if self.nd_solutions: + self._nobj = len(self.nd_solutions[0].fobj) + for p in self.nd_solutions: + f = () + for i in range(self._nobj): + f += (p.fobj[i],) + ftemp.append(f) + return ftemp + + def normalize_data(self, pareto_front, reference_point): + """ + Normalize Pareto points and the reference point. + + :param pareto_front: List of Pareto points where each point is a tuple (x, y). + :param reference_point: The reference point (rx, ry). + :return: Normalized Pareto points and reference point. + """ + # Convert Pareto front and reference point to numpy arrays + pareto_front = np.array(pareto_front) + reference_point = np.array(reference_point) + + # Find min and max values for each objective + min_vals = np.min(pareto_front, axis=0) + max_vals = np.max(pareto_front, axis=0) + + # Ensure that min and max values are not the same to avoid division by zero + if np.any(max_vals == min_vals): + raise ValueError("Max and min values for at least one objective are the same. Normalization cannot be performed.") + + # Normalize Pareto points + normalized_pareto_front = (pareto_front - min_vals) / (max_vals - min_vals) + + # Normalize reference point + normalized_reference_point = (reference_point - min_vals) / (max_vals - min_vals) + + return normalized_pareto_front, normalized_reference_point def hypervolume(self): """ @@ -34,29 +76,96 @@ def hypervolume(self): Returns: - The hypervolume indicator value. """ - self.find_ref_point() - # self.ND_solutions = np.array(self.ND_solutions) - # self._ref_point = np.array(self._ref_point) - - # Ensure all objectives are minimized (convert to maximization problem) - ND_solutions = np.array([np.subtract(self._ref_point.f, xf.f) for xf in self.ND_solutions]) + if not self.ref_point: + self.find_ref_point() + ref_p = tuple(self.ref_point.coordinates) + pf = self.get_pareto_points() + pf_n, ref_n = self.normalize_data(pareto_front=pf, reference_point=ref_p) + # Create a Hypervolume object with the reference point + pf_n_list = [] + for i in range(len(pf_n)): + pf_n_list.append(list(pf_n[i])) + + pf_n_list = np.array(pf_n_list) + return hv.hypervolume(pointset=pf_n_list, ref=np.array(list(ref_n))) + + # # Ensure all objectives are minimized (convert to maximization problem) + # nd_solutions = np.array([np.subtract(self._ref_point.f, xf.f) for xf in self.nd_solutions]) - # Sort self.ND_solutionss lexicographically - ND_solutions.sort(axis=0) + # # Sort self.ND_solutionss lexicographically + # nd_solutions.sort(axis=0) - hypervolume_value = 0.0 - last_volume = [1.0]*self.nobj + # hypervolume_value = 0.0 + # last_volume = [1.0]*self.nobj - for point in ND_solutions: - current_volume = 1.0 - for i in range(len(self._ref_point.f)): - current_volume *= max(last_volume[i], point[i]) - last_volume[i] + # for point in nd_solutions: + # current_volume = 1.0 + # for i in range(len(self._ref_point.f)): + # current_volume *= max(last_volume[i], point[i]) - last_volume[i] - hypervolume_value += current_volume - last_volume = point + # hypervolume_value += current_volume + # last_volume = point - return hypervolume_value + # return hypervolume_value + + def normalize_data(self, pareto_front, reference_point): + """ + Normalize Pareto points and the reference point. + + :param pareto_front: List of Pareto points where each point is a tuple (x, y). + :param reference_point: The reference point (rx, ry). + :return: Normalized Pareto points and reference point. + """ + # Convert Pareto front and reference point to numpy arrays + pareto_front = np.array(pareto_front) + reference_point = np.array(reference_point) + + # Find min and max values for each objective + min_vals = np.min(pareto_front, axis=0) + max_vals = np.max(pareto_front, axis=0) + + # Normalize Pareto points + normalized_pareto_front = (pareto_front - min_vals) / (max_vals - min_vals) + + # Normalize reference point + normalized_reference_point = (reference_point - min_vals) / (max_vals - min_vals) + + return normalized_pareto_front, normalized_reference_point + def calculate_hypervolume(self, pf, rp): + """ + Calculate the hypervolume of a bi-objective Pareto front. + + :param pareto_front: A list of Pareto points where each point is a tuple (x, y). + :param reference_point: The reference point (rx, ry) to compute the hypervolume against. + :return: Hypervolume of the Pareto front. + """ + # Sort Pareto front by the first objective (x-coordinate) + pareto_front, reference_point = self.normalize_data(pf, rp) + pareto_front = sorted(pareto_front, key=lambda point: point[0]) + + # Initialize variables + hypervolume = 0.0 + previous_y = reference_point[1] + + # Iterate through the sorted Pareto points + for i in range(len(pareto_front)): + x, y = pareto_front[i] + # Compute the area between the current point and the previous point + width = pareto_front[i][0] - (pareto_front[i - 1][0] if i > 0 else 0) + height = previous_y - y + hypervolume += width * height + + # Update previous_y to the current y + previous_y = y + + # Account for the last segment up to the reference point + width = reference_point[0] - pareto_front[-1][0] + height = previous_y - reference_point[1] + hypervolume += width * height + + return hypervolume + def generational_distance(self, true_pareto_front, approximate_pareto_front): """ Compute the generational distance (GD) metric between two Pareto fronts. @@ -97,8 +206,8 @@ def inverted_generational_distance(self, true_pareto_front, approximate_pareto_f igd = igd_sum / len(true_pareto_front) return igd - def dominates(self, A, B): - return all(A <= B) and any(A < B) + def dominates(self, a, b): + return all(a <= b) and any(a < b) def ranking(self, solutions): # Initialize ranks @@ -108,8 +217,7 @@ def ranking(self, solutions): # Compare each solution with every other solution for i in range(n): for j in range(n): - if i != j: - if self.dominates(solutions[j], solutions[i]): + if i != j and self.dominates(solutions[j], solutions[i]): rank[i] += 1 diff --git a/src/OMADS/Omesh.py b/src/OMADS/Omesh.py index 874d687..23c27ec 100644 --- a/src/OMADS/Omesh.py +++ b/src/OMADS/Omesh.py @@ -1,10 +1,12 @@ -import copy -from typing import List -from ._globals import * +from typing import List, Optional + +import numpy as np +from ._globals import DType, VAR_TYPE, GL_LIMITS from .Point import Point from .Mesh import Mesh from .Options import Options from .Parameters import Parameters +from dataclasses import dataclass @dataclass class Omesh(Mesh): @@ -20,27 +22,27 @@ class Omesh(Mesh): :param _dtype: numpy double data type precision """ - _n: int = None - _meshSize: Point = None #1.0 # mesh size - _frameSize: Point = 1.0 # poll size - _rho: List[float] = 1.0 # poll size to mesh size ratio + _n: Optional[int] = None + _meshSize: Optional[Point] = None #1.0 # mesh size + _frameSize: Optional[Point] = None # poll size + _rho: Optional[List[float]] = None # poll size to mesh size ratio # Completed: manage the poll size granularity for discrete variables # A new class 'Gmesh' is now avialable. # Gmesh adapts mesh granularity and anistropy # See: Audet et. al, The mesh adaptive direct search algorithm for # granular and discrete variable - _exp: Point = None - _mantissa: Point = None - _maximumFrameSize: Point = None - successfulFrameSize: Point = None + _exp: Optional[Point] = None + _mantissa: Optional[Point] = None + _maximumFrameSize: Optional[Point] = None + successfulFrameSize: Optional[Point] = None # numpy double data type precision - _dtype: DType = None + _dtype: Optional[DType] = None - def __init__(self, pbParam: Parameters, runOptions: Options): + def __init__(self, pb_param: Parameters, run_options: Options): """ Constructor """ - super(Omesh, self).__init__(pbParams=pbParam, limitMaxMeshIndex=-GL_LIMITS, limitMinMeshIndex=GL_LIMITS) - self._n = len(pbParam.baseline) + super(Omesh, self).__init__(pb_params=pb_param, limit_max_mesh_index=-GL_LIMITS, limit_min_mesh_index=GL_LIMITS) + self._n = len(pb_param.baseline) self.meshSize = Point(self._n) self.frameSize = Point(self._n) self._exp = Point(self._n) @@ -48,7 +50,7 @@ def __init__(self, pbParam: Parameters, runOptions: Options): self._maximumFrameSize = Point(self._n) self.successfulFrameSize = Point(self._n) self.rho = [0] * self._n - self.frameSize.coordinates = runOptions.psize_init if isinstance(runOptions.psize_init, list) else [runOptions.psize_init] * self._n + self.frameSize.coordinates = run_options.psize_init if isinstance(run_options.psize_init, list) else [run_options.psize_init] * self._n self.meshSize.reset(n=self._n, d=0) self._r = Point(self._n) self._r.coordinates = [1]*self._n diff --git a/src/OMADS/Optimizer.py b/src/OMADS/Optimizer.py index 3d491f3..50baeda 100644 --- a/src/OMADS/Optimizer.py +++ b/src/OMADS/Optimizer.py @@ -1,55 +1,58 @@ +import numpy as np from .CandidatePoint import CandidatePoint from .Point import Point -from .Barriers import * +from .Barriers import BarrierMO from ._common import logger from dataclasses import dataclass, field -from typing import List, Dict, Any, Protocol +from typing import List, Protocol, Optional from .Gmesh import Gmesh from .Cache import Cache from .Evaluator import Evaluator import samplersLib as explore - +from ._globals import MPP, DType +from .Parameters import Parameters +from .Options import Options @dataclass class ConstraintsRelaxationParameters: RHO: float = MPP.RHO LAMBDA: List[float] = field(default_factory=lambda: [MPP.LAMBDA]) hmax: float = 1. - constraints_type: List[int] = None + constraints_type: Optional[List[int]] = None @dataclass class GenericSamplerBaseData(Protocol): scaling: List[List[float]] = field(default_factory=list) hashtable: Cache = field(default_factory=Cache) - mesh: Gmesh = None + mesh: Optional[Gmesh] = None bb_handle: Evaluator = field(default_factory=Evaluator) - Failure_stop: bool = None + Failure_stop: Optional[bool] = None constraintsHandler: ConstraintsRelaxationParameters = field(default_factory=lambda: ConstraintsRelaxationParameters) - log: logger = None + log: Optional[logger] = None n_successes: int = 0 - prob_params: Parameters = None + prob_params: Optional[Parameters] = None sampling_t: int = 3 vicinity_ratio: np.ndarray = None vicinity_min: float = 0.001 terminate: bool =False visualize: bool = False - sampling_criter: str = None - weights: List[float] = None + sampling_criter: Optional[str] = None + weights: Optional[List[float]] = None AS: explore.samplers.activeSampling = None best_samples: int = 0 estGrid: explore.samplers.sampling = None - activeBarrier: BarrierMO = None + activeBarrier: Optional[BarrierMO] = None constraints_RP: ConstraintsRelaxationParameters = field(default_factory=lambda: ConstraintsRelaxationParameters(RHO=MPP.RHO.value, LAMBDA=None, hmax=1., constraints_type=None)) - _evalSet: List[CandidatePoint] = None - _points: List[Point] = None - _pointsIndex: List[int] = None + _evalSet: Optional[List[CandidatePoint]] = None + _points: Optional[List[Point]] = None + _pointsIndex: Optional[List[int]] = None _n: int = 0 _candidate_points_set : List[CandidatePoint] = field(default_factory=list) _point_index: List[int] = field(default_factory=list) _directions_set: List[Point] = field(default_factory=list) _defined: List[bool] = field(default_factory=lambda: [False]) - _xmin: CandidatePoint = None - _x_sc: CandidatePoint = None + _xmin: Optional[CandidatePoint] = None + _x_sc: Optional[CandidatePoint] = None _nb_success: int = 0 _bb_eval: int = field(default_factory=int) _psize: float = field(default_factory=float) @@ -60,7 +63,7 @@ class GenericSamplerBaseData(Protocol): _save_results = True _opportunistic: bool = False _eval_budget: int = 100 - _dtype: DType = None + _dtype: Optional[DType] = None _success: bool = False _seed: int = 0 _terminate: bool = False @@ -91,15 +94,15 @@ def update(self): ... @dataclass -class genericGlobalLocalSamplerBaseData(Protocol): - localSearch: GenericSamplerBase = None - globalSearch: GenericSamplerBase = None - param: Parameters = None - options: Options = None +class GenericGlobalLocalSamplerBaseData(Protocol): + localSearch: Optional[GenericSamplerBase] = None + globalSearch: Optional[GenericSamplerBase] = None + param: Optional[Parameters] = None + options: Optional[Options] = None @dataclass -class genericGlobalLocalSamplerBase(genericGlobalLocalSamplerBaseData, Protocol): +class GenericGlobalLocalSamplerBase(GenericGlobalLocalSamplerBaseData, Protocol): def generate_candidate_points(self)->List[CandidatePoint]: ... diff --git a/src/OMADS/Options.py b/src/OMADS/Options.py index d7dadbd..534da3b 100644 --- a/src/OMADS/Options.py +++ b/src/OMADS/Options.py @@ -1,9 +1,5 @@ from dataclasses import dataclass -import logging from typing import Any -import numpy as np -from .Point import Point -from ._globals import * @dataclass class Options: diff --git a/src/OMADS/POLL.py b/src/OMADS/POLL.py index 984457a..098d24e 100644 --- a/src/OMADS/POLL.py +++ b/src/OMADS/POLL.py @@ -26,25 +26,22 @@ """ import copy import importlib -import json from multiprocessing import freeze_support import os -import pkgutil import sys import numpy as np -import concurrent.futures import time -from typing import List, Dict, Any +from typing import List, Dict, Any, Optional +import importlib.util if importlib.util.find_spec('BMDFO'): from BMDFO import toy from .Point import Point from .Barriers import Barrier, BarrierMO -from ._common import * -from .Directions import * -from .PrePoll import * +from ._common import validator, logger +from .PrePoll import PrePoll from .CandidatePoint import CandidatePoint -from .PostProcess import Output, PostMADS - +from ._globals import DESIGN_STATUS, MSG_TYPE, SUCCESS_TYPES, VAR_TYPE +from .Metrics import Metrics np.set_printoptions(legacy='1.21') def main(*args) -> Dict[str, Any]: @@ -52,24 +49,24 @@ def main(*args) -> Dict[str, Any]: """ Validate and parse the parameters file """ validate = validator() - data: dict = validate.checkInputFile(args=args) + data: dict = validate.check_input_file(args=args) """ Initialize the log file """ log = logger() if not os.path.exists(data["param"]["post_dir"]): try: os.mkdir(data["param"]["post_dir"]) - except: + except Warning: os.makedirs(data["param"]["post_dir"], exist_ok=True) log.initialize(data["param"]["post_dir"] + "/OMADS.log") """ Run preprocessor for the setup of the optimization problem and for the initialization of optimization process """ - iteration, xmin, poll, options, param, post, out, B, outP = PrePoll(data).initialize_from_dict(log=log) - out.stepName = "Poll" - if outP: - outP.stepName = "Poll_ND" + iteration, xmin, poll, options, param, post, out, B, out_p = PrePoll(data).initialize_from_dict(log=log) + out.step_name = "Poll" + if out_p: + out_p.step_name = "Poll_ND" """ Set the random seed for results reproducibility """ if len(args) < 4: @@ -80,12 +77,12 @@ def main(*args) -> Dict[str, Any]: """ Start the count down for calculating the runtime indicator """ tic = time.perf_counter() peval = poll.bb_handle.bb_eval - LAMBDA_k = xmin.LAMBDA - RHO_k = xmin.RHO + lambda_k = xmin.lambda_multipliers + rho_k = xmin.rho while True: del poll.poll_set poll.mesh.update() - poll.constraints_RP.LAMBDA = copy.deepcopy(xmin.LAMBDA) + poll.constraints_RP.LAMBDA = copy.deepcopy(xmin.lambda_multipliers) poll.constraints_RP.constraints_type = copy.deepcopy(poll.xmin.constraints_type) """ Create the set of poll directions """ hhm = poll.create_housholder(options.rich_direction, domain=xmin.var_type) @@ -100,22 +97,21 @@ def main(*args) -> Dict[str, Any]: else: B.insert(xmin) elif isinstance(B, BarrierMO) and iteration == 1: - B.init(evalPointList=[xmin]) + B.init(eval_point_list=[xmin]) if isinstance(B, Barrier): - poll.constraints_RP.hmax = xmin.hmax + poll.constraints_RP.hmax = xmin.h_max poll.create_poll_set(hhm=hhm, ub=param.ub, lb=param.lb, it=iteration, var_type=xmin.var_type, var_sets=xmin.sets, var_link = xmin.var_link, c_types=param.constraints_type, is_prim=True) if B._sec_poll_center is not None and B._sec_poll_center.evaluated: del poll.poll_set - # poll.poll_dirs = [] poll.x_sc = B._sec_poll_center poll.create_poll_set(hhm=hhm, ub=param.ub, lb=param.lb, it=iteration, var_type=B._sec_poll_center.var_type, var_sets=B._sec_poll_center.sets, var_link = B._sec_poll_center.var_link, c_types=param.constraints_type, is_prim=False) elif isinstance(B, BarrierMO): - poll.constraints_RP.hmax = B._hMax + poll.constraints_RP.hmax = B._h_max del poll.poll_set del poll.poll_dirs if B._currentIncumbentFeas and B._currentIncumbentFeas.evaluated: @@ -128,7 +124,6 @@ def main(*args) -> Dict[str, Any]: lb=param.lb, it=iteration, var_type=poll.xmin.var_type, var_sets=poll.xmin.sets, var_link = poll.xmin.var_link, c_types=param.constraints_type, is_prim=True) if B._currentIncumbentInf and B._currentIncumbentInf.evaluated: - # del poll.poll_set poll.x_sc = B._currentIncumbentInf poll.create_poll_set(hhm=hhm, ub=param.ub, @@ -139,8 +134,8 @@ def main(*args) -> Dict[str, Any]: lb=param.lb, it=iteration, var_type=poll.xmin.var_type, var_sets=poll.xmin.sets, var_link = poll.xmin.var_link, c_types=param.constraints_type, is_prim=False) - poll.constraints_RP.LAMBDA = LAMBDA_k - poll.constraints_RP.RHO = RHO_k + poll.constraints_RP.LAMBDA = lambda_k + poll.constraints_RP.RHO = rho_k """ Save current poll directions and incumbent solution so they can be saved later in the post dir """ @@ -153,18 +148,18 @@ def main(*args) -> Dict[str, Any]: poll.bb_output = [] xt = [] """ Serial evaluation for points in the poll set """ - if log is not None and log.isVerbose: + if log and log.is_verbose: log.log_msg(f"----------- Evaluate poll set # {iteration}-----------", msg_type=MSG_TYPE.INFO) poll.log = log if options.check_cache: poll.omit_duplicates() poll.bb_handle.xmin = poll.xmin if not options.parallel_mode: - xt, post, peval = poll.bb_handle.run_callable_serial_local(iter=iteration, peval=peval, eval_set=poll.poll_set, options=options, post=post, psize=poll.mesh.getDeltaFrameSize().coordinates, constraintsRelaxation=poll.constraints_RP.__dict__, budget=options.budget) + xt, post, peval = poll.bb_handle.run_callable_serial_local(iter=iteration, peval=peval, eval_set=poll.poll_set, options=options, post=post, psize=poll.mesh.getDeltaFrameSize().coordinates, constraints_relaxation=poll.constraints_RP.__dict__, budget=options.budget) else: poll.point_index = -1 """ Parallel evaluation for points in the poll set """ - poll.bb_eval, xt, post, peval = poll.bb_handle.run_callable_parallel_local(iter=iteration, peval=peval, njobs=options.np, eval_set=poll.poll_set, options=options, post=post, psize=poll.mesh.getDeltaFrameSize().coordinates, constraintsRelaxation=poll.constraints_RP.__dict__, budget=options.budget) + poll.bb_eval, xt, post, peval = poll.bb_handle.run_callable_parallel_local(iter=iteration, peval=peval, eval_set=poll.poll_set, options=options, post=post, psize=poll.mesh.getDeltaFrameSize().coordinates, constraints_relaxation=poll.constraints_RP.__dict__, budget=options.budget) poll.postprocess_evaluated_candidates(xt) if isinstance(B, Barrier): xpost: List[CandidatePoint] = poll.master_updates(xt, peval, save_all_best=options.save_all_best, save_all=options.save_results) @@ -184,37 +179,35 @@ def main(*args) -> Dict[str, Any]: for p in poll.poll_set: if p.evaluated: pev += 1 - # if pev != poll.poll_dirs and not poll.success: - # poll.seed += 1 - goToSearch: bool = (pev == 0 and poll.Failure_stop is not None and poll.Failure_stop) + + go_to_search: bool = (pev == 0 and poll.Failure_stop is not None and poll.Failure_stop) - dir: Point = Point(poll._n) - dir.coordinates = poll.xmin.direction.coordinates if poll.xmin.direction is not None else [0]*poll._n - if poll.success == SUCCESS_TYPES.FS and not goToSearch: - poll.mesh.enlargeDeltaFrameSize(direction=dir) # poll.mesh.psize = np.multiply(poll.mesh.psize, 2, dtype=poll.dtype.dtype + direction: Point = Point(poll._n) + direction.coordinates = poll.xmin.direction.coordinates if poll.xmin.direction is not None else [0]*poll._n + if poll.success == SUCCESS_TYPES.FS and not go_to_search: + poll.mesh.enlargeDeltaFrameSize(direction=direction) # poll.mesh.psize = np.multiply(poll.mesh.psize, 2, dtype=poll.dtype.dtype elif poll.success == SUCCESS_TYPES.US: poll.mesh.refineDeltaFrameSize() - # poll.mesh.psize = np.divide(poll.mesh.psize, 2, dtype=poll.dtype.dtype) elif isinstance(B, BarrierMO): xpost: List[CandidatePoint] = [] for i in range(len(xt)): xpost.append(xt[i]) - updated, _, _ = B.updateWithPoints(evalPointList=xpost, evalType=None, keepAllPoints=False, updateInfeasibleIncumbentAndHmax=True) + updated, _, _ = B.updateWithPoints(eval_point_list=xpost, keep_all_points=False) if not updated: - newMesh = None + new_mesh = None if B._currentIncumbentInf: B._currentIncumbentInf.mesh.refineDeltaFrameSize() - newMesh = copy.deepcopy(B._currentIncumbentFeas.mesh) if B._currentIncumbentFeas else copy.deepcopy(B._currentIncumbentInf.mesh) if B._currentIncumbentInf else None + new_mesh = copy.deepcopy(B._currentIncumbentFeas.mesh) if B._currentIncumbentFeas else copy.deepcopy(B._currentIncumbentInf.mesh) if B._currentIncumbentInf else None B.updateCurrentIncumbents() if B._currentIncumbentFeas: B._currentIncumbentFeas.mesh.refineDeltaFrameSize() - newMesh = copy.deepcopy(B._currentIncumbentFeas.mesh) if B._currentIncumbentFeas else copy.deepcopy(B._currentIncumbentInf.mesh) if B._currentIncumbentInf else None + new_mesh = copy.deepcopy(B._currentIncumbentFeas.mesh) if B._currentIncumbentFeas else copy.deepcopy(B._currentIncumbentInf.mesh) if B._currentIncumbentInf else None B.updateCurrentIncumbents() - if newMesh: - poll.mesh = newMesh + if new_mesh: + poll.mesh = new_mesh else: poll.mesh.refineDeltaFrameSize() @@ -226,38 +219,45 @@ def main(*args) -> Dict[str, Any]: post.xmin = B._currentIncumbentFeas if B._currentIncumbentFeas else B._currentIncumbentInf if B._currentIncumbentInf else poll.xmin poll.mesh.update() - if log is not None: + if log: log.log_msg(msg=post.__str__(), msg_type=MSG_TYPE.INFO) if options.display: print(post) - LAMBDA_k = poll.constraints_RP.LAMBDA - RHO_k = poll.constraints_RP.RHO + lambda_k = poll.constraints_RP.LAMBDA + rho_k = poll.constraints_RP.RHO if options.save_results: post.nd_points = [] - post.output_results(out, allRes=False) + post.output_results(out, all_res=False) if param.isPareto: for i in range(len(B.getAllPoints())): post.nd_points.append(B.getAllPoints()[i]) - post.output_nd_results(outP) + post.output_nd_results(out_p) - Failure_check = iteration > 0 and poll.Failure_stop is not None and poll.Failure_stop and (poll.success == SUCCESS_TYPES.US or goToSearch) + failure_check = iteration > 0 and poll.Failure_stop is not None and poll.Failure_stop and (poll.success == SUCCESS_TYPES.US or go_to_search) - if (Failure_check or poll.bb_eval >= options.budget) or (all(abs(poll.mesh.getDeltaFrameSize().coordinates[pp]) < options.tol for pp in range(poll._n)) or poll.bb_eval >= options.budget or poll.terminate): - log.log_msg(f"\n--------------- Termination of the poll step ---------------", MSG_TYPE.INFO) + if (failure_check or poll.bb_eval >= options.budget) or (all(abs(poll.mesh.getDeltaFrameSize().coordinates[pp]) < options.tol for pp in range(poll._n)) or poll.bb_eval >= options.budget or poll.terminate): + log.log_msg("\n--------------- Termination of the poll step ---------------", MSG_TYPE.INFO) if all(abs(poll.mesh.getDeltaFrameSize().coordinates[pp]) < options.tol for pp in range(poll._n)): log.log_msg("Termination criterion hit: the mesh size is below the minimum threshold defined.", MSG_TYPE.INFO) if (poll.bb_eval >= options.budget or poll.terminate): log.log_msg("Termination criterion hit: evaluation budget is exhausted.", MSG_TYPE.INFO) - if (Failure_check): - log.log_msg(f"Termination criterion hit (optional): failed to find a successful point in iteration # {iteration}.", MSG_TYPE.INFO) - log.log_msg(f"---------------------------------------------------------------\n", MSG_TYPE.INFO) + if (failure_check): + log.log_msg("Termination criterion hit (optional): failed to find a successful point in iteration # {iteration}.", MSG_TYPE.INFO) + log.log_msg("---------------------------------------------------------------\n", MSG_TYPE.INFO) break iteration += 1 toc = time.perf_counter() + if isinstance(B, BarrierMO): + rp: Optional[CandidatePoint] = None + if param.ref_point: + rp = Point() + rp.coordinates = param.ref_point + perf_m = Metrics(nd_solutions=B.getAllPoints(), nobj=B._nobj, ref_point=rp) + HV = perf_m.hypervolume() """ If benchmarking, then populate the results in the benchmarking output report """ if importlib.util.find_spec('BMDFO') and len(args) > 1 and isinstance(args[1], toy.Run): @@ -281,27 +281,28 @@ def main(*args) -> Dict[str, Any]: print(f"{poll.bb_handle.blackbox}: fmin = {poll.xmin.f} , hmin= {poll.xmin.h:.2f}") elif importlib.util.find_spec('BMDFO') and len(args) > 1 and not isinstance(args[1], toy.Run): - if log is not None: - log.log_msg(msg="Could not find " + args[1] + " in the internal BM suite.", msg_type=MSG_TYPE.ERROR) - raise IOError("Could not find " + args[1] + " in the internal BM suite.") + temp = " in the internal BM suite." + if log: + log.log_msg(msg="Could not find " + args[1] + temp, msg_type=MSG_TYPE.ERROR) + raise IOError("Could not find " + args[1] + temp) if options.display: print(" end of orthogonal MADS ") - if log is not None: + if log: log.log_msg(msg=" end of orthogonal MADS ", msg_type=MSG_TYPE.INFO) print(" Final objective value: " + str(poll.xmin.f) + ", hmin= " + str(poll.xmin.h)) - if log is not None: + if log: log.log_msg(msg=" Final objective value: " + str(poll.xmin.f) + ", hmin= " + str(poll.xmin.h), msg_type=MSG_TYPE.INFO) - if log is not None and len(args)>1 and isinstance(args[1], str): + if log and len(args)>1 and isinstance(args[1], str): log.log_msg(msg=" end of orthogonal MADS running" + args[1] + " in the internal BM suite.", msg_type=MSG_TYPE.INFO) if options.save_coordinates: post.output_coordinates(out) - if log is not None: + if log: log.log_msg(msg="\n---Run Summary---", msg_type=MSG_TYPE.INFO) log.log_msg(msg=f" Run completed in {toc - tic:.4f} seconds", msg_type=MSG_TYPE.INFO) log.log_msg(msg=f" Random numbers generator's seed {options.seed}", msg_type=MSG_TYPE.INFO) @@ -349,11 +350,12 @@ def main(*args) -> Dict[str, Any]: "psize": poll.mesh.getDeltaFrameSize().coordinates, "psuccess": poll.xmin.mesh.getDeltaFrameSize().coordinates, # "pmax": poll.mesh.psize_max, - "msize": poll.mesh.getdeltaMeshSize().coordinates} + "msize": poll.mesh.getdeltaMeshSize().coordinates, + "HV": HV if param.isPareto else "NA"} return output, poll -def rosen(x, p, *argv): +def rosen(x, p): x = np.asarray(x) y = [np.sum(p[0] * (x[1:] - x[:-1] ** p[1]) ** p[1] + (1 - x[:-1]) ** p[1], axis=0), [0]] @@ -363,10 +365,10 @@ def alpine(x): y = [abs(x[0]*np.sin(x[0])+0.1*x[0])+abs(x[1]*np.sin(x[1])+0.1*x[1]), [0]] return y -def Ackley3(x): +def ackley3(x): return [-200*np.exp(-0.2*np.sqrt(x[0]**2+x[1]**2))+5*np.exp(np.cos(3*x[0])+np.sin(3*x[1])), [0]] -def eggHolder(individual): +def egg_holder(individual): x = individual[0] y = individual[1] f = (-(y + 47.0) * np.sin(np.sqrt(abs(x/2.0 + (y + 47.0)))) - x * np.sin(np.sqrt(abs(x - (y + 47.0))))) diff --git a/src/OMADS/Parameters.py b/src/OMADS/Parameters.py index 26f05e3..b91cb1a 100644 --- a/src/OMADS/Parameters.py +++ b/src/OMADS/Parameters.py @@ -1,10 +1,10 @@ from dataclasses import dataclass -import logging import os -from typing import List, Dict +from typing import List, Dict, Optional +import warnings import numpy as np from .Point import Point -from ._globals import * +from ._globals import DType, VAR_TYPE, BARRIER_TYPES, MESH_TYPE import copy @dataclass @@ -18,39 +18,42 @@ class Parameters: :param scaling: Scaling factor (can be defined as a list (assigning a factor for each variable) or a scalar value that will be applied on all variables) :param post_dir: The location and name of the post directory where the output results file will live in (if any) """ - _n: int = None - baseline: List[float] = None - lb: List[float] = None - ub: List[float] = None - var_names: List[str] = None - fun_names: List[str] = None - scaling: List[float] = None - post_dir: str = os.path.abspath("./") - var_type: List[str] = None - var_sets: Dict = None - constants: List = None - constants_name: List = None - Failure_stop: bool = None + _n: Optional[int] = None + baseline: Optional[List[float]] = None + lb: Optional[List[float]] = None + ub: Optional[List[float]] = None + var_names: Optional[List[str]] = None + fun_names: Optional[List[str]] = None + scaling: Optional[List[float]] = None + post_dir: Optional[str] = os.path.abspath("./") + var_type: Optional[List[str]] = None + var_sets: Optional[Dict] = None + constants: Optional[List] = None + constants_name: Optional[List] = None + failure_stop: Optional[bool] = None problem_name: str = "unknown" - best_known: List[float] = None - constraints_type: List[BARRIER_TYPES] = None - function_weights: List[float] = None + best_known: Optional[List[float]] = None + constraints_type: Optional[List[BARRIER_TYPES]] = None + function_weights: Optional[List[float]] = None h_max: float = 0 RHO: float = 0.00005 - LAMBDA: List[float] = None + LAMBDA: Optional[List[float]] = None name: str = "undefined" nobj: int = 1 + ref_point: Optional[List[float]] = None + lhs_search_initialization: Optional[bool] = False + # Mesh options meshType: str = MESH_TYPE.ORTHO.name - fixed_variables: Point = None - granularity: Point = None - minMeshSize: Point = None - minFrameSize: Point = None - initialMeshSize: Point = None - initialFrameSize: Point = None + fixed_variables: Optional[Point] = None + granularity: Optional[Point] = None + minMeshSize: Optional[Point] = None + minFrameSize: Optional[Point] = None + initialMeshSize: Optional[Point] = None + initialFrameSize: Optional[Point] = None warningInitialFrameSizeReset: bool = True - x0: Point = None + x0: Optional[Point] = None _initialized_and_checked: bool = False isPareto: bool = False incumbentincumbentSelectionParam: int = 1 @@ -88,7 +91,9 @@ def __init__( isPareto: bool = False, nobj: int=1, incumbentincumbentSelectionParam: int=1, - barrierInitializedFromCache:bool =True): + barrierInitializedFromCache:bool =True, + ref_point: List[float]=None, + lhs_search_initialization: bool = False): self.incumbentincumbentSelectionParam = incumbentincumbentSelectionParam self.barrierInitializedFromCache = barrierInitializedFromCache self.nobj = nobj @@ -106,7 +111,7 @@ def __init__( self.var_type = var_type self.constants = constants self.constants_name = constants_name - self.Failure_stop: bool = Failure_stop + self.failure_stop: bool = Failure_stop self.problem_name = problem_name self.best_known = best_known self.constraints_type = constraints_type @@ -116,6 +121,7 @@ def __init__( self.name = name self.var_sets = var_sets self.isPareto = isPareto + self.lhs_search_initialization = lhs_search_initialization # Mesh options self.meshType = meshType point_init = Point() @@ -194,19 +200,20 @@ def __init__( if self.var_type is None or len(self.var_type) <= 0: self.var_type = [VAR_TYPE.REAL.name] * self.n - self.setMinMeshParameters() - self.setMinFrameParameters() - self.setInitialMeshParameters() - self.x0.checkForGranularity(g=self.granularity, name="baseline") - self.minMeshSize.checkForGranularity(g=self.granularity, name="minMeshSize") - self.minFrameSize.checkForGranularity(g=self.granularity, name="minFrameSize") - self.initialMeshSize.checkForGranularity(g=self.granularity, name="initialMeshSize") - self.initialFrameSize.checkForGranularity(g=self.granularity, name="initialFrameSize") + self.set_min_mesh_parameters() + self.set_min_frame_parameters() + self.set_initial_mesh_parameters() + self.x0.check_for_granularity(g=self.granularity, name="baseline") + self.minMeshSize.check_for_granularity(g=self.granularity, name="minMeshSize") + self.minFrameSize.check_for_granularity(g=self.granularity, name="minFrameSize") + self.initialMeshSize.check_for_granularity(g=self.granularity, name="initialMeshSize") + self.initialFrameSize.check_for_granularity(g=self.granularity, name="initialFrameSize") self._initialized_and_checked = True + self.ref_point = ref_point - def setInitialMeshParameters(self): + def set_initial_mesh_parameters(self): if self.initialMeshSize.is_all_defined() and self.initialMeshSize.size != self.n: raise IOError(f"INITIAL_MESH_SIZE has dimension {self.initialMeshSize.size} which is different from problem dimension {self.n}") @@ -235,7 +242,7 @@ def setInitialMeshParameters(self): self.warningInitialFrameSizeReset = False warnings.warn("Initial frame size reset from initial mesh") self.minFrameSize[i] = self.initialMeshSize[i] * np.power(self.n, 0.5) - self.initialFrameSize[i] = self.initialFrameSize.nextMult(g=self.granularity[i], i=i) + self.initialFrameSize[i] = self.initialFrameSize.next_mult(g=self.granularity[i], i=i) if self.initialFrameSize[i] < self.minFrameSize[i]: self.initialFrameSize[i] = self.minFrameSize[i] @@ -252,7 +259,7 @@ def setInitialMeshParameters(self): else: self.initialFrameSize[i] = 1.0 # Adjust value with granularity - self.initialFrameSize[i] = self.initialFrameSize.nextMult(g=self.granularity[i], i=i) + self.initialFrameSize[i] = self.initialFrameSize.next_mult(g=self.granularity[i], i=i) # Adjust value with minFrameSize if self.initialFrameSize[i] < self.minFrameSize[i]: self.initialFrameSize[i] = self.minFrameSize[i] @@ -260,21 +267,21 @@ def setInitialMeshParameters(self): if not self.initialMeshSize.defined[i]: self.initialMeshSize[i] = self.initialFrameSize[i] * self.n**-0.5 # Adjust value with granularity - self.initialMeshSize[i] = self.initialMeshSize.nextMult(g=self.granularity[i], i=i) + self.initialMeshSize[i] = self.initialMeshSize.next_mult(g=self.granularity[i], i=i) # Adjust value with minMeshSize if (self.initialMeshSize[i] < self.minMeshSize[i]): self.initialMeshSize[i] = self.minMeshSize[i] - if not (self.minMeshSize[i] <= self.initialMeshSize[i]): + if (self.minMeshSize[i] > self.initialMeshSize[i]): raise IOError("Check: initial mesh size is lower than min mesh size.\n" + f"INITIAL_MESH_SIZE + {self.initialMeshSize[i]} \n" + f"MIN_MESH_SIZE {self.minMeshSize[i]}") - if not (self.minFrameSize[i] <= self.minFrameSize[i]): + if (self.minFrameSize[i] > self.minFrameSize[i]): raise IOError("Check: initial frame size is lower than min frame size.\n" + f"INITIAL_FRAME_SIZE + {self.minFrameSize[i]} \n" + f"MIN_FRAME_SIZE {self.minFrameSize[i]}") - def setMinMeshParameters(self): + def set_min_mesh_parameters(self): if not self.minMeshSize.is_all_defined(): for i in range(self.n): if self.granularity[i] > 0.0: @@ -291,7 +298,7 @@ def setMinMeshParameters(self): else: raise IOError("Error: granularity is defined with a negative value.") - def setMinFrameParameters(self): + def set_min_frame_parameters(self): if not self.minFrameSize.is_all_defined(): for i in range(self.n): if self.granularity[i] > 0.0: @@ -308,7 +315,7 @@ def setMinFrameParameters(self): else: raise IOError("Error: granularity is defined with a negative value.") - def toBeChecked(self)-> bool: + def to_be_checked(self)-> bool: return self._initialized_and_checked # TODO: give better control on variabls' resolution (mesh granularity) diff --git a/src/OMADS/Point.py b/src/OMADS/Point.py index 28f3941..6099e83 100644 --- a/src/OMADS/Point.py +++ b/src/OMADS/Point.py @@ -22,11 +22,11 @@ # ------------------------------------------------------------------------------------# import copy -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import List, Dict, Any, Optional -from numpy import sum, subtract, add, maximum, power, inf +from numpy import subtract, add import numpy as np -from ._globals import * +from ._globals import DType @dataclass class Point: @@ -41,23 +41,23 @@ class Point: # Dimension of the point _n: int = 0 # Coordinates of the point - _coords: List[float] = None + _coords: Optional[List[float]] = None # Coordinates definition boolean - _defined: List[bool] = None + _defined: Optional[List[bool]] = None # Evaluation boolean _evaluated: bool = False # hash signature, in the cache memory _signature: int = 0 # numpy double data type precision - _dtype: DType = None + _dtype: Optional[DType] = None # Variables type - _var_type: List[int] = None + _var_type: Optional[List[int]] = None # Discrete set - _sets: Dict = None + _sets: Optional[Dict] = None source: str = "Current run" - Model: str = "Simulation" + model_type: str = "Simulation" def __post_init__(self): self._dtype = DType() @@ -139,9 +139,9 @@ def push_back(self, val: Any): else: self.coordinates = self._coords + [val]*self._n - def checkForGranularity(self, g: Any, name: str) -> bool: + def check_for_granularity(self, g: Any, name: str) -> bool: for i in range(self._n): - if not self.isMult(self.coordinates[i], g[i]): + if not self.is_mult(self.coordinates[i], g[i]): raise IOError("Check: Invalid granularity of parameter " + name + f"at index {i} : {self.coordinates[i]} vs granularity value {g[i]} found a non-zero remainder of {self.coordinates[i] % g[i]}.") return True @@ -193,7 +193,7 @@ def reset(self, n: int = 0, d: Optional[float] = 0): self.defined = [False] * n - def nextMult(self, g: float = None, i: int = 0) -> float: + def next_mult(self, g: float = None, i: int = 0) -> float: d: float # Calculate the remainder when number is divided by multiple_of # Calculate the ratio to find next multiple_of @@ -202,19 +202,17 @@ def nextMult(self, g: float = None, i: int = 0) -> float: # # Calculate the next multiple_of # next_multiple = ratio * self.coordinates[i] value = self.coordinates[i] - if g is None or not self.defined[i] or g <= 0. or self.isMult(value, g): + if g is None or not self.defined[i] or g <= 0. or self.is_mult(value, g): d = value else: # granularity > 0, and _value is not a multiple of granularity. # Adjust value with granularity - granMult = round(abs(value)/g) + gran_mult = round(abs(value)/g) if value > 0: - granMult += 1 - # if abs(value) > 0: - # granMult += granMult - d = granMult*g + gran_mult += 1 + d = gran_mult*g - if not self.isMult(d, g): + if not self.is_mult(d, g): raise IOError("nextMult(gran): cannot get a multiple of granularity") # trials = 0 # while (not self.isMult(d, g)): @@ -228,39 +226,37 @@ def nextMult(self, g: float = None, i: int = 0) -> float: return d - def previousMult(self, g: float, i: int): + def previous_mult(self, g: float = None, i: int = -1): d: float - if g is not None or not self.is_all_defined() or g <= 0. or self.isMult(self.coordinates[i], g): + if g is not None or not self.is_all_defined() or g <= 0. or self.is_mult(self.coordinates[i], g): d = self.coordinates[i] else: - granMult: int = int(self.coordinates[i]/g) + gran_mult: int = int(self.coordinates[i]/g) if self.coordinates[i] < 0: - granMult-= 1 - bigGranExp: int = 10 ** self.nDecimals(g) - bigGran: int = int(g*bigGranExp) - d = granMult * bigGran/bigGranExp + gran_mult-= 1 + big_gran_exp: int = 10 ** self.n_decimals(g) + big_gran: int = int(g*big_gran_exp) + d = gran_mult * big_gran/big_gran_exp return d - def isMult(self, v1: float, v2: float): - isMult: bool = True + def is_mult(self, v1: float, v2: float): + is_mult: bool = True if abs(v1) <= self.dtype.zero: - isMult = True + is_mult = True elif (abs(v2) > 0): mult = round(v1/v2) verif_value = mult * v2 if abs(v1-verif_value) < abs(mult)*self.dtype.zero: - isMult = True + is_mult = True elif v2 < 0: - isMult = False + is_mult = False else: - isMult = True + is_mult = True - return isMult - - # return ((v1%v2) <= self.dtype.zero) if v2 > 0.0 else True + return is_mult - def nDecimals(self, n: float): + def n_decimals(self, n: float): return len(n.rsplit('.')[-1]) if '.' in n else 0 @@ -268,13 +264,13 @@ def __eq__(self, other) -> bool: return self.size is other.size and other.coordinates is self.coordinates \ and self.is_any_defined() is other.is_any_defined() - def __le__(self, other) -> bool: + def __le__(self, other) -> Optional[bool]: if self.size is other._n and self.is_all_defined() is other.is_all_defined(): return all(self.coordinates[i] <= other.coordinates[i] for i in range(self._n)) else: return None - def __lt__(self, other) -> bool: + def __lt__(self, other) -> Optional[bool]: if self.size is other._n and self.is_all_defined() is other.is_all_defined(): return all(self.coordinates[i] < other.coordinates[i] for i in range(self._n)) else: diff --git a/src/OMADS/PostProcess.py b/src/OMADS/PostProcess.py index f70e9f6..543ce64 100644 --- a/src/OMADS/PostProcess.py +++ b/src/OMADS/PostProcess.py @@ -1,10 +1,8 @@ from dataclasses import dataclass, field -import importlib import os -from typing import List, Dict, Any +from typing import List, Dict, Any, Optional from .CandidatePoint import CandidatePoint import json -from ._globals import * import csv @dataclass @@ -19,7 +17,7 @@ class Output: pname: str = "MADS0" runfolder: str = "undefined" replace: bool = True - stepName: str = "Poll" + step_name: str = "Poll" suffix: str = "all" def __post_init__(self): @@ -64,20 +62,18 @@ def clear_csv_content(self): def add_row(self, eval_time: int, iterno: int, evalno: int, source: str, - Mname: str, + m_name: str, poll_size: float, status: str, - fobj: float, - h: float, f: float, rho: float, L: List[float], hmax: float, - x: List[float], stepName: str, fnames: List[str]): - row = {f'{"Runtime (Sec)".rjust(25)}': f'{f"{eval_time}".rjust(25)}', f'{"Iteration".rjust(25)}': f'{f"{iterno}".rjust(25)}', f'{"Evaluation #".rjust(25)}': f'{f"{evalno}".rjust(25)}', f'{"Step".rjust(25)}': f'{f"{stepName}".rjust(25)}', f'{"Source".rjust(25)}': f'{f"{source}".rjust(25)}', f'{"Model_name".rjust(25)}': f'{f"{Mname}".rjust(25)}', f'{"Delta".rjust(25)}': f'{f"{min(poll_size)}".rjust(25)}', f'{"Status".rjust(25)}': f'{f"{status}".rjust(25)}', f'{"phi".rjust(25)}': f'{f"{max(f)}".rjust(25)}'} + fobj: Any, + h: float, f: float, rho: float, lambdas: List[float], hmax: float, + x: List[float], step_name: str, fnames: List[str]): + row = {f'{"Runtime (Sec)".rjust(25)}': f'{f"{eval_time}".rjust(25)}', f'{"Iteration".rjust(25)}': f'{f"{iterno}".rjust(25)}', f'{"Evaluation #".rjust(25)}': f'{f"{evalno}".rjust(25)}', f'{"Step".rjust(25)}': f'{f"{step_name}".rjust(25)}', f'{"Source".rjust(25)}': f'{f"{source}".rjust(25)}', f'{"Model_name".rjust(25)}': f'{f"{m_name}".rjust(25)}', f'{"Delta".rjust(25)}': f'{f"{min(poll_size)}".rjust(25)}', f'{"Status".rjust(25)}': f'{f"{status}".rjust(25)}', f'{"phi".rjust(25)}': f'{f"{max(f)}".rjust(25)}'} for i in range(len(fobj)): row.update({f'{f"{fnames[i]}".rjust(25)}': f'{f"{fobj[i]}".rjust(25)}'}) - row.update({f'{"max(c_in)".rjust(25)}': f'{f"{h}".rjust(25)}', f'{"Penalty_parameter".rjust(25)}': f'{f"{rho}".rjust(25)}', f'{"Multipliers".rjust(25)}': f'{f"{max(L) if len(L)>0 else None}".rjust(25)}', f'{"hmax".rjust(25)}': f'{f"{hmax}".rjust(25)}'}) + row.update({f'{"max(c_in)".rjust(25)}': f'{f"{h}".rjust(25)}', f'{"Penalty_parameter".rjust(25)}': f'{f"{rho}".rjust(25)}', f'{"Multipliers".rjust(25)}': f'{f"{max(lambdas) if len(lambdas)>0 else None}".rjust(25)}', f'{"hmax".rjust(25)}': f'{f"{hmax}".rjust(25)}'}) - # row = {'Iter no.': iterno, 'Eval no.': evalno, - # 'poll_size': poll_size, 'hmin': h, 'fmin': f} ss = 0 for k in range(13+len(fnames), len(self.field_names)): row[self.field_names[k]] = f'{f"{x[ss]}".rjust(25)}' @@ -99,28 +95,28 @@ class PostMADS: iter: List[int] = field(default_factory=list) bb_eval: List[int] = field(default_factory=list) psize: List[float] = field(default_factory=list) - step_name: List[str] = None + step_name: Optional[List[str]] = None nd_points: List[CandidatePoint] = field(default_factory=list) counter: int = 0 - def output_results(self, out: Output, allRes: bool = True): + def output_results(self, out: Output, all_res: bool = True): """ Create a results file from the saved cache""" - if allRes: + if all_res: self.counter = 0 for p in self.poll_dirs[self.counter:]: if p.evaluated and self.counter < len(self.iter): - out.add_row(eval_time= p.Eval_time, + out.add_row(eval_time= p.eval_time, iterno=self.iter[self.counter], evalno=self.bb_eval[self.counter], poll_size=self.psize[self.counter], source=p.source, - Mname=p.Model, + m_name=p.model, f=p.f, status=p.status.name, h=max(p.c_ineq), fobj=p.fobj, - rho=p.RHO, - L=p.LAMBDA, + rho=p.rho, + lambdas=p.lambda_multipliers, x=p.coordinates, - hmax=p.hmax, stepName="Poll-2n" if self.step_name is None else self.step_name[self.counter], fnames=out.fnames) + hmax=p.h_max, step_name="Poll-2n" if self.step_name is None else self.step_name[self.counter], fnames=out.fnames) self.counter += 1 def output_nd_results(self, out: Output): @@ -129,19 +125,19 @@ def output_nd_results(self, out: Output): out.clear_csv_content() for p in self.nd_points: if p.evaluated and counter < len(self.iter): - out.add_row(eval_time= p.Eval_time, + out.add_row(eval_time= p.eval_time, iterno=self.iter[counter], - evalno= p.evalNo, poll_size=self.psize[counter], + evalno= p.eval_no, poll_size=self.psize[counter], source=p.source, - Mname=p.Model, + m_name=p.model, f=p.f, status=p.status.name, h=max(p.c_ineq), fobj=p.fobj, - rho=p.RHO, - L=p.LAMBDA, + rho=p.rho, + lambdas=p.lambda_multipliers, x=p.coordinates, - hmax=p.hmax, stepName="Poll-2n" if self.step_name is None else self.step_name[counter], fnames=out.fnames) + hmax=p.h_max, step_name="Poll-2n" if self.step_name is None else self.step_name[counter], fnames=out.fnames) counter += 1 def output_coordinates(self, out: Output): diff --git a/src/OMADS/PreExploration.py b/src/OMADS/PreExploration.py index c5c88a4..f55975c 100644 --- a/src/OMADS/PreExploration.py +++ b/src/OMADS/PreExploration.py @@ -20,18 +20,28 @@ # https://github.com/Ahmed-Bayoumy/OMADS # # Copyright (C) 2022 Ahmed H. Bayoumy # # ------------------------------------------------------------------------------------# -from .Exploration import * -from typing import Callable +from .Exploration import efficient_exploration, search_sampling from .Parameters import Parameters from .Options import Options from .Omesh import Omesh from multiprocessing import cpu_count from .PostProcess import PostMADS, Output +from dataclasses import dataclass +from typing import Dict, Any, List, Optional +from ._common import logger +from .CandidatePoint import CandidatePoint +from ._globals import MSG_TYPE, VAR_TYPE +import copy +from .Barriers import BarrierMO, Barrier +from .Evaluator import Evaluator +from .Gmesh import Gmesh +from .Cache import Cache + @dataclass class PreExploration: """ Preprocessor for setting up optimization settings and parameters""" data: Dict[Any, Any] - log: logger = None + log: Optional[logger] = None def initialize_from_dict(self, log: logger = None, xs: CandidatePoint=None): """ MADS initialization """ """ 1- Construct the following classes by unpacking @@ -42,7 +52,7 @@ def initialize_from_dict(self, log: logger = None, xs: CandidatePoint=None): self.log.log_msg(msg="- Reading the input dictionaries", msg_type=MSG_TYPE.INFO) options = Options(**self.data["options"]) param = Parameters(**self.data["param"]) - log.isVerbose = options.isVerbose + log.is_verbose = options.isVerbose B = BarrierMO(param=param, options=options) if param.isPareto else Barrier(param) ev = Evaluator(**self.data["evaluator"]) if self.log is not None: @@ -51,23 +61,9 @@ def initialize_from_dict(self, log: logger = None, xs: CandidatePoint=None): ev.dtype.precision = options.precision if param.constants != None: ev.constants = copy.deepcopy(param.constants) - - # if param.constraints_type is not None and isinstance(param.constraints_type, list): - # for i in range(len(param.constraints_type)): - # if param.constraints_type[i] == BARRIER_TYPES.PB.name: - # param.constraints_type[i] = BARRIER_TYPES.PB - # elif param.constraints_type[i] == BARRIER_TYPES.RB.name: - # param.constraints_type[i] = BARRIER_TYPES.RB - # elif param.constraints_type[i] == BARRIER_TYPES.PEB.name: - # param.constraints_type[i] = BARRIER_TYPES.PEB - # else: - # param.constraints_type[i] = BARRIER_TYPES.EB - # elif param.constraints_type is not None: - # param.constraints_type = BARRIER_TYPES(param.constraints_type) """ 2- Initialize iteration number and construct a point instant for the starting point """ iteration: int = 0 - x_start = CandidatePoint() """ 3- Construct an instant for the poll 2n orthogonal directions class object """ extend = options.extend is not None and isinstance(options.extend, efficient_exploration) is_xs = False @@ -79,14 +75,14 @@ def initialize_from_dict(self, log: logger = None, xs: CandidatePoint=None): if not extend: search = efficient_exploration() search.prob_params = copy.deepcopy(param) - if param.Failure_stop != None and isinstance(param.Failure_stop, bool): - search.Failure_stop = param.Failure_stop + if param.failure_stop != None and isinstance(param.failure_stop, bool): + search.Failure_stop = param.failure_stop search._candidate_points_set = [] search.dtype.precision = options.precision search.save_results = options.save_results """ 4- Construct an instant for the mesh subclass object by inheriting initial parameters from mesh_params() """ - search.mesh = Gmesh(pbParam=param, runOptions=options) if (param.meshType).lower() == "gmesh" else Omesh(pbParam=param, runOptions=options) + search.mesh = Gmesh(pb_param=param, run_options=options) if (param.meshType).lower() == "gmesh" else Omesh(pb_param=param, run_options=options) search.sampling_t = search_step.s_method search.type = search_step.type @@ -97,8 +93,6 @@ def initialize_from_dict(self, log: logger = None, xs: CandidatePoint=None): """ 5- Assign optional algorithmic parameters to the constructed poll instant """ search.opportunistic = options.opportunistic search.seed = options.seed - # search.mesh.dtype.precision = options.precision - # search.mesh.psize = options.psize_init search.eval_budget = options.budget search.store_cache = options.store_cache search.check_cache = options.check_cache @@ -110,7 +104,7 @@ def initialize_from_dict(self, log: logger = None, xs: CandidatePoint=None): search._candidate_points_set = [] n_available_cores = cpu_count() if options.parallel_mode and options.np > n_available_cores: - options.np == n_available_cores + options.np = n_available_cores """ 6- Initialize blackbox handling subclass by copying the evaluator 'ev' instance to the poll object """ search.bb_handle = ev @@ -160,12 +154,11 @@ def initialize_from_dict(self, log: logger = None, xs: CandidatePoint=None): elif k.lower()[0] == "o": x_start.var_type.append(VAR_TYPE.ORDINAL) x_start.var_link.append(None) - # TODO: Implementation in progress + # COMPLETED: Implementation in progress elif k.lower()[0] == "b": x_start.var_type.append(VAR_TYPE.BINARY) else: - x_start.var_type.append(VAR_TYPE.REAL) - x_start.var_link.append(None) + raise IOError("Could not recognize the variable of type " + k + ". Please use on of the following keywords to identify your variable type: real, integer, discrete, categorical, ordinal, or binary") x_start.dtype.precision = options.precision @@ -180,9 +173,9 @@ def initialize_from_dict(self, log: logger = None, xs: CandidatePoint=None): else: if not is_xs: search.bb_output, _ = search.bb_handle.eval(x_start.coordinates) - x_start.hmax = B._h_max if isinstance(B, Barrier) else B._hMax - search.hmax = B._h_max if isinstance(B, Barrier) else B._hMax - x_start.RHO = param.RHO + x_start.h_max = B._h_max + search.hmax = B._h_max + x_start.rho = param.RHO if param.LAMBDA is None: param.LAMBDA = [0] * len(x_start.c_ineq) if not isinstance(param.LAMBDA, list): @@ -191,14 +184,12 @@ def initialize_from_dict(self, log: logger = None, xs: CandidatePoint=None): param.LAMBDA += [param.LAMBDA[-1]] * abs(len(param.LAMBDA)-len(x_start.c_ineq)) if len(x_start.c_ineq) < len(param.LAMBDA): del param.LAMBDA[len(x_start.c_ineq):] - x_start.LAMBDA = param.LAMBDA + x_start.lambda_multipliers = param.LAMBDA x_start.constraints_type = param.constraints_type if not is_xs: x_start.__eval__(search.bb_output) - if isinstance(B, Barrier): - B._h_max = x_start.hmax - elif isinstance(B, BarrierMO): - B._hMax = x_start.hmax + if isinstance(B, Barrier) or isinstance(B, BarrierMO): + B._h_max = x_start.h_max """ 9- Copy the starting point object to the poll's minimizer subclass """ x_start.mesh = copy.deepcopy(search.mesh) search.xmin = copy.deepcopy(x_start) @@ -210,9 +201,9 @@ def initialize_from_dict(self, log: logger = None, xs: CandidatePoint=None): if not extend: search.hashtable = Cache() search.hashtable._n_dim = len(param.baseline) - search.hashtable._isPareto = param.isPareto + search.hashtable._is_pareto = param.isPareto if param.isPareto: - search.hashtable.ND_points = [] + search.hashtable.nd_points = [] """ 10- Initialize the number of successful points found and check if the starting minimizer performs better than the worst (f = inf) """ @@ -222,8 +213,8 @@ def initialize_from_dict(self, log: logger = None, xs: CandidatePoint=None): search.mesh.psize_max =copy.deepcopy(max(search.mesh.getDeltaFrameSize().coordinates)) search._candidate_points_set = [search.xmin] """ 11- Construct the results postprocessor class object 'post' """ - x_start.evalNo = search.bb_handle.bb_eval - search.xmin.evalNo = search.bb_handle.bb_eval + x_start.eval_no = search.bb_handle.bb_eval + search.xmin.eval_no = search.bb_handle.bb_eval post = PostMADS(x_incumbent=[search.xmin], xmin=search.xmin, poll_dirs=[search.xmin]) post.step_name = [] post.step_name.append(f'Search: {search.type}') @@ -235,21 +226,20 @@ def initialize_from_dict(self, log: logger = None, xs: CandidatePoint=None): """ Note: printing the post will print a results row within the results table shown in Python console if the 'display' option is true """ - # if options.display: - # print(post) + """ 12- Add the starting point hash value to the cache memory """ if options.store_cache: search.hashtable.hash_id = x_start """ 13- Initialize the output results file object """ out = Output(file_path=param.post_dir, vnames=param.var_names, fnames=param.fun_names, pname=param.name, runfolder=f'{param.name}_run', replace=True) if param.isPareto: - outP = Output(file_path=param.post_dir, vnames=param.var_names, fnames=param.fun_names, pname=param.name, runfolder=f'{param.name}_ND', suffix="Pareto") + out_p = Output(file_path=param.post_dir, vnames=param.var_names, fnames=param.fun_names, pname=param.name, runfolder=f'{param.name}_ND', suffix="Pareto") else: - outP = None + out_p = None if options.display: print("End of the evaluation of the starting points") if self.log is not None: self.log.log_msg(msg="- End of the evaluation of the starting points.", msg_type=MSG_TYPE.INFO) iteration += 1 - return iteration, x_start, search, options, param, post, out, B, outP + return iteration, x_start, search, options, param, post, out, B, out_p diff --git a/src/OMADS/PrePoll.py b/src/OMADS/PrePoll.py index b69fc87..4e7cfc3 100644 --- a/src/OMADS/PrePoll.py +++ b/src/OMADS/PrePoll.py @@ -23,20 +23,25 @@ from .CandidatePoint import CandidatePoint from .Barriers import Barrier, BarrierMO -# from ._common import * from .Omesh import Omesh -from .Directions import * +from .Directions import Dirs2n from .Parameters import Parameters from .Options import Options from .Evaluator import Evaluator from multiprocessing import cpu_count from .PostProcess import PostMADS, Output - +from typing import Any, Dict, List, Optional +from dataclasses import dataclass +from ._common import logger +import copy +from ._globals import VAR_TYPE, MSG_TYPE, DESIGN_STATUS +from .Gmesh import Gmesh +from .Cache import Cache @dataclass class PrePoll: """ Preprocessor for setting up optimization settings and parameters""" data: Dict[Any, Any] - log: logger = None + log: Optional[logger] = None def initialize_from_dict(self, log: logger = None, xs: CandidatePoint=None): """ MADS initialization """ @@ -48,7 +53,7 @@ def initialize_from_dict(self, log: logger = None, xs: CandidatePoint=None): self.log.log_msg(msg="- Reading the input dictionaries", msg_type=MSG_TYPE.INFO) options = Options(**self.data["options"]) param = Parameters(**self.data["param"]) - log.isVerbose = options.isVerbose + log.is_verbose = options.isVerbose B = BarrierMO(param=param, options=options) if param.isPareto else Barrier(param) ev = Evaluator(**self.data["evaluator"]) if self.log is not None: @@ -72,18 +77,16 @@ def initialize_from_dict(self, log: logger = None, xs: CandidatePoint=None): if not extend: """ 3- Construct an instant for the poll 2n orthogonal directions class object """ poll = Dirs2n() - if param.Failure_stop != None and isinstance(param.Failure_stop, bool): - poll.Failure_stop = param.Failure_stop + if param.failure_stop != None and isinstance(param.failure_stop, bool): + poll.Failure_stop = param.failure_stop poll.dtype.precision = options.precision """ 4- Construct an instant for the mesh subclass object by inheriting initial parameters from mesh_params() """ # COMPLETED: Add the Gmesh constructor req inputs - poll.mesh = Gmesh(pbParam=param, runOptions=options) if (param.meshType).lower() == "gmesh" else Omesh(pbParam=param, runOptions=options) + poll.mesh = Gmesh(pb_param=param, run_options=options) if (param.meshType).lower() == "gmesh" else Omesh(pb_param=param, run_options=options) """ 5- Assign optional algorithmic parameters to the constructed poll instant """ poll.opportunistic = options.opportunistic poll.seed = options.seed - # poll.mesh.dtype.precision = options.precision - # poll.mesh.psize = options.psize_init poll.eval_budget = options.budget poll.store_cache = options.store_cache poll.check_cache = options.check_cache @@ -94,7 +97,7 @@ def initialize_from_dict(self, log: logger = None, xs: CandidatePoint=None): n_available_cores = cpu_count() if options.parallel_mode and options.np > n_available_cores: - options.np == n_available_cores + options.np = n_available_cores """ 6- Initialize blackbox handling subclass by copying the evaluator 'ev' instance to the poll object""" poll.bb_handle = ev @@ -142,13 +145,11 @@ def initialize_from_dict(self, log: logger = None, xs: CandidatePoint=None): elif k.lower()[0] == "o": x_start.var_type.append(VAR_TYPE.ORDINAL) x_start.var_link.append(None) - # TODO: Implementation in progress + # COMPLETED: Implementation in progress elif k.lower()[0] == "b": x_start.var_type.append(VAR_TYPE.BINARY) else: - x_start.var_type.append(VAR_TYPE.REAL) - x_start.var_link.append(None) - + raise IOError("Could not recognize the variable of type " + k + ". Please use on of the following keywords to identify your variable type: real, integer, discrete, categorical, ordinal, or binary") x_start.dtype.precision = options.precision if x_start.sets is not None and isinstance(x_start.sets,dict): @@ -163,8 +164,8 @@ def initialize_from_dict(self, log: logger = None, xs: CandidatePoint=None): else: if not is_xs: poll.bb_output, _ = poll.bb_handle.eval(x_start.coordinates) - x_start.hmax = B._h_max if isinstance(B, Barrier) else B._hMax - x_start.RHO = param.RHO + x_start.h_max = B._h_max + x_start.rho = param.RHO if param.LAMBDA is None: param.LAMBDA = [0] * len(x_start.c_ineq) @@ -174,14 +175,12 @@ def initialize_from_dict(self, log: logger = None, xs: CandidatePoint=None): param.LAMBDA += [param.LAMBDA[-1]] * abs(len(param.LAMBDA)-len(x_start.c_ineq)) if len(x_start.c_ineq) < len(param.LAMBDA): del param.LAMBDA[len(x_start.c_ineq):] - x_start.LAMBDA = param.LAMBDA + x_start.lambda_multipliers = param.LAMBDA if not is_xs: x_start.__eval__(poll.bb_output) - if isinstance(B, Barrier): - B._h_max = x_start.hmax - elif isinstance(B, BarrierMO): - B._hMax = x_start.hmax + if isinstance(B, Barrier) or isinstance(B, BarrierMO): + B._h_max = x_start.h_max """ 9- Copy the starting point object to the poll's minimizer subclass """ if not extend: if x_start.status == DESIGN_STATUS.INFEASIBLE and isinstance(B, BarrierMO): @@ -196,9 +195,9 @@ def initialize_from_dict(self, log: logger = None, xs: CandidatePoint=None): if not extend: poll.hashtable = Cache() poll.hashtable._n_dim = len(param.baseline) - poll.hashtable._isPareto = param.isPareto + poll.hashtable._is_pareto = param.isPareto if param.isPareto: - poll.hashtable.ND_points = [] + poll.hashtable.nd_points = [] """ 10- Initialize the number of successful points found and check if the starting minimizer performs better than the worst (f = inf) """ @@ -221,16 +220,16 @@ def initialize_from_dict(self, log: logger = None, xs: CandidatePoint=None): """ 11- Construct the results postprocessor class object 'post' """ if poll.xmin.evaluated: - x_start.evalNo = poll.bb_handle.bb_eval - poll.xmin.evalNo = poll.bb_handle.bb_eval + x_start.eval_no = poll.bb_handle.bb_eval + poll.xmin.eval_no = poll.bb_handle.bb_eval post = PostMADS(x_incumbent=[poll.xmin], xmin=poll.xmin, poll_dirs=[poll.xmin]) post.psize.append(poll.mesh.getDeltaFrameSize().coordinates) post.bb_eval.append(poll.bb_handle.bb_eval) post.iter.append(iteration) elif poll.x_sc.evaluated: - x_start.evalNo = poll.bb_handle.bb_eval - poll.x_sc.evalNo = poll.bb_handle.bb_eval + x_start.eval_no = poll.bb_handle.bb_eval + poll.x_sc.eval_no = poll.bb_handle.bb_eval post = PostMADS(x_incumbent=[poll.x_sc], xmin=poll.x_sc, poll_dirs=[poll.x_sc]) post.psize.append(poll.mesh.getDeltaFrameSize().coordinates) post.bb_eval.append(poll.bb_handle.bb_eval) @@ -240,17 +239,15 @@ def initialize_from_dict(self, log: logger = None, xs: CandidatePoint=None): """ Note: printing the post will print a results row within the results table shown in Python console if the 'display' option is true """ - # if options.display: - # print(post) """ 12- Add the starting point hash value to the cache memory """ if options.store_cache: poll.hashtable.hash_id = x_start """ 13- Initialize the output results file object """ out = Output(file_path=param.post_dir, vnames=param.var_names, fnames=param.fun_names, pname=param.name, runfolder=f'{param.name}_run', suffix="all") if param.isPareto: - outP = Output(file_path=param.post_dir, vnames=param.var_names, fnames=param.fun_names, pname=param.name, runfolder=f'{param.name}_ND', suffix="Pareto") + out_p = Output(file_path=param.post_dir, vnames=param.var_names, fnames=param.fun_names, pname=param.name, runfolder=f'{param.name}_ND', suffix="Pareto") else: - outP = None + out_p = None if options.display: print("End of the evaluation of the starting points") if self.log is not None: @@ -258,4 +255,4 @@ def initialize_from_dict(self, log: logger = None, xs: CandidatePoint=None): iteration += 1 - return iteration, x_start, poll, options, param, post, out, B, outP + return iteration, x_start, poll, options, param, post, out, B, out_p diff --git a/src/OMADS/SEARCH.py b/src/OMADS/SEARCH.py index ccc988f..eec4d0a 100644 --- a/src/OMADS/SEARCH.py +++ b/src/OMADS/SEARCH.py @@ -22,24 +22,26 @@ # ------------------------------------------------------------------------------------# import importlib -import json from multiprocessing import freeze_support import os import sys import time import numpy as np import copy -from typing import List, Dict, Any -import concurrent.futures +from typing import List, Dict, Any, Optional from matplotlib import pyplot as plt if importlib.util.find_spec('BMDFO'): from BMDFO import toy from .CandidatePoint import CandidatePoint -from ._common import * -from .Directions import * -from .Exploration import * -from .PreExploration import * +from ._common import logger, validator +from .Exploration import SAMPLING_METHOD, VNS +from .PreExploration import PreExploration +from ._globals import SEARCH_TYPE, VAR_TYPE, DESIGN_STATUS, MSG_TYPE, SUCCESS_TYPES +from .Barriers import Barrier, BarrierMO +from .Point import Point +from .Metrics import Metrics + np.set_printoptions(legacy='1.21') def main(*args) -> Dict[str, Any]: @@ -47,24 +49,24 @@ def main(*args) -> Dict[str, Any]: """ Validate and parse the parameters file """ validate = validator() - data: dict = validate.checkInputFile(args=args) + data: dict = validate.check_input_file(args=args) """ Initialize the log file """ log = logger() if not os.path.exists(data["param"]["post_dir"]): try: os.mkdir(data["param"]["post_dir"]) - except: + except Warning: os.makedirs(data["param"]["post_dir"], exist_ok=True) log.initialize(data["param"]["post_dir"] + "/OMADS.log") """ Run preprocessor for the setup of the optimization problem and for the initialization of optimization process """ - iteration, xmin, search, options, param, post, out, B, outP = PreExploration(data).initialize_from_dict(log=log) + iteration, xmin, search, options, param, post, out, B, out_p = PreExploration(data).initialize_from_dict(log=log) - if outP: - outP.stepName = "Search_ND" + if out_p: + out_p.step_name = "Search_ND" """ Set the random seed for results reproducibility """ if len(args) < 4: @@ -72,18 +74,7 @@ def main(*args) -> Dict[str, Any]: else: np.random.seed(int(args[3])) - out.stepName = f"Search: {search.type}" - - - """ Initialize the visualization figure""" - if search.visualize: - plt.ion() - fig = plt.figure() - ax=[] - nplots = len(param.var_names)-1 - ps = [None]*nplots**2 - for ii in range(nplots**2): - ax.append(fig.add_subplot(nplots, nplots, ii+1)) + out.step_name = f"Search: {search.type}" """ Start the count down for calculating the runtime indicator """ tic = time.perf_counter() @@ -91,17 +82,14 @@ def main(*args) -> Dict[str, Any]: peval = 0 if search.type == SEARCH_TYPE.VNS.name: - search_VN = VNS(active_barrier=B, params=param) - search_VN._ns_dist = [int(((search.dim+1)/2)*((search.dim+2)/2)/(len(search_VN._dist))) if search.ns is None else search.ns] * len(search_VN._dist) - search.ns = sum(search_VN._ns_dist) + search_vn = VNS(active_barrier=B, params=param) + search_vn._ns_dist = [int(((search.dim+1)/2)*((search.dim+2)/2)/(len(search_vn._dist))) if search.ns is None else search.ns] * len(search_vn._dist) + search.ns = sum(search_vn._ns_dist) search.lb = param.lb search.ub = param.ub - LAMBDA_k = xmin.LAMBDA - RHO_k = xmin.RHO - log.log_msg(msg=f"---------------- Run the SEARCH step ({search.sampling_t}) ----------------", msg_type=MSG_TYPE.INFO) num_strat: int = 0 while True: @@ -116,7 +104,7 @@ def main(*args) -> Dict[str, Any]: else: B.insert(search.xmin) elif isinstance(B, BarrierMO) and iteration == 1: - B.init(evalPointList=[xmin]) + B.init(eval_point_list=[xmin]) if isinstance(B, Barrier): search.hmax = B._h_max @@ -131,49 +119,26 @@ def main(*args) -> Dict[str, Any]: """ Create the set of poll directions """ if search.type == SEARCH_TYPE.VNS.name: - search_VN.active_barrier = B - search._candidate_points_set = search_VN.run() - if search_VN.stop: + search_vn.active_barrier = B + search._candidate_points_set = search_vn.run() + if search_vn.stop: print("Reached maximum number of VNS iterations!") break - vv = search.map_samples_from_coords_to_points(samples=search._candidate_points_set) + search.map_samples_from_coords_to_points(samples=search._candidate_points_set) else: - vvp = vvs = [] - bestFeasible: CandidatePoint = B._currentIncumbentFeas if isinstance(B, BarrierMO) else B._best_feasible - bestInf: CandidatePoint = B._currentIncumbentInf if isinstance(B, BarrierMO) else B.get_best_infeasible() - if bestFeasible is not None and bestFeasible.evaluated: - search.xmin = bestFeasible - vvp, _ = search.generate_sample_points(int(((search.dim+1)/2)*((search.dim+2)/2)) if search.ns is None else search.ns) - if bestInf is not None and bestInf.evaluated: + best_feasible: CandidatePoint = B._currentIncumbentFeas if isinstance(B, BarrierMO) else B._best_feasible + best_inf: CandidatePoint = B._currentIncumbentInf if isinstance(B, BarrierMO) else B.get_best_infeasible() + if best_feasible is not None and best_feasible.evaluated: + search.xmin = best_feasible + search.generate_sample_points(int(((search.dim+1)/2)*((search.dim+2)/2)) if search.ns is None else search.ns) + if best_inf is not None and best_inf.evaluated: # if B._filter is not None and B.get_best_infeasible().evaluated: xmin_bup = search.xmin - Prim_samples = search._candidate_points_set - search.xmin = bestInf - vvs, _ = search.generate_sample_points(int(((search.dim+1)/2)*((search.dim+2)/2)) if search.ns is None else search.ns) - search._candidate_points_set += Prim_samples + prim_samples = search._candidate_points_set + search.xmin = best_inf + search.generate_sample_points(int(((search.dim+1)/2)*((search.dim+2)/2)) if search.ns is None else search.ns) + search._candidate_points_set += prim_samples search.xmin = xmin_bup - - if isinstance(vvs, list) and len(vvs) > 0: - vv = vvp + vvs - else: - vv = vvp - - if search.visualize: - sc_old = search.store_cache - cc_old = search.check_cache - search.check_cache = False - search.store_cache = False - for iii in range(len(ax)): - for jjj in range(len(xmin.coordinates)): - for kkk in range(jjj, len(xmin.coordinates)): - if kkk != jjj: - if all([psi is None for psi in ps]): - xinput = [search.xmin] - else: - xinput = search._candidate_points_set - ps = visualize(xinput, jjj, kkk, search.mesh.getdeltaMeshSize().coordinates, vv, fig, ax, search.xmin, ps, bbeval=search.bb_handle, lb=search.prob_params.lb, ub=search.prob_params.ub, spindex=iii, bestKnown=search.prob_params.best_known, blk=False) - search.store_cache = sc_old - search.check_cache = cc_old """ Save current poll directions and incumbent solution @@ -187,22 +152,23 @@ def main(*args) -> Dict[str, Any]: search.bb_output = [] xt = [] """ Serial evaluation for points in the poll set """ - if log is not None and log.isVerbose: + if log and log.is_verbose: log.log_msg(f"----------- Evaluate Search iteration # {iteration}-----------", msg_type=MSG_TYPE.INFO) search.log = log if options.check_cache: search.omit_duplicates() search.bb_handle.xmin = xmin if not options.parallel_mode: - xt, post, peval = search.bb_handle.run_callable_serial_local(iter=iteration, peval=peval, eval_set=search._candidate_points_set, options=options, post=post, psize=search.mesh.getDeltaFrameSize().coordinates, stepName=f'Search: {search.type}', mesh=search.mesh, constraintsRelaxation=search.constraints_RP.__dict__, budget=options.budget) + xt, post, peval = search.bb_handle.run_callable_serial_local(iter=iteration, peval=peval, eval_set=search._candidate_points_set, options=options, post=post, psize=search.mesh.getDeltaFrameSize().coordinates, step_name=f'Search: {search.type}', mesh=search.mesh, constraints_relaxation=search.constraints_RP.__dict__, budget=options.budget) else: """ Parallel evaluation for points in the samples set """ search.point_index = -1 - search.bb_eval, xt, post, peval = search.bb_handle.run_callable_parallel_local(iter=iteration, peval=peval, njobs=options.np, eval_set=search._candidate_points_set, options=options, post=post, mesh=search.mesh, stepName=f'Search: {search.type}', psize=search.mesh.getDeltaFrameSize().coordinates, constraintsRelaxation=search.constraints_RP.__dict__, budget=options.budget) + search.bb_eval, xt, post, peval = search.bb_handle.run_callable_parallel_local(iter=iteration, peval=peval, eval_set=search._candidate_points_set, options=options, post=post, mesh=search.mesh, step_name=f'Search: {search.type}', psize=search.mesh.getDeltaFrameSize().coordinates, constraints_relaxation=search.constraints_RP.__dict__, budget=options.budget) search.postprocess_evaluated_candidates(xt) - + if iteration == 1: + search.vicinity_ratio = np.ones((len(search.xmin.coordinates),1)) if isinstance(B, Barrier): xpost: List[CandidatePoint] = search.master_updates(xt, peval, save_all_best=options.save_all_best, save_all=options.save_results) if options.save_results: @@ -215,20 +181,17 @@ def main(*args) -> Dict[str, Any]: """ Update the xmin in post""" post.xmin = copy.deepcopy(search.xmin) - if iteration == 1: - search.vicinity_ratio = np.ones((len(search.xmin.coordinates),1)) + """ Updates """ if search.success == SUCCESS_TYPES.FS: - dir: Point = Point(search.mesh._n) - dir.coordinates = search.xmin.direction.coordinates - # search.mesh.psize = np.multiply(search.mesh.get, 2, dtype=search.dtype.dtype) - search.mesh.enlargeDeltaFrameSize(direction=dir) + direction: Point = Point(search.mesh._n) + direction.coordinates = search.xmin.direction.coordinates + search.mesh.enlargeDeltaFrameSize(direction=direction) if search.sampling_t != SAMPLING_METHOD.ACTIVE.name: search.update_local_region(region="expand") elif search.success == SUCCESS_TYPES.US: - # search.mesh.psize = np.divide(search.mesh.psize, 2, dtype=search.dtype.dtype) search.mesh.refineDeltaFrameSize() if search.sampling_t != SAMPLING_METHOD.ACTIVE.name: search.update_local_region(region="contract") @@ -236,40 +199,40 @@ def main(*args) -> Dict[str, Any]: xpost: List[CandidatePoint] = [] for i in range(len(xt)): xpost.append(xt[i]) - updated, updatedF, updatedInf = B.updateWithPoints(evalPointList=xpost, evalType=None, keepAllPoints=False, updateInfeasibleIncumbentAndHmax=True) + updated, updated_f, updated_inf = B.updateWithPoints(eval_point_list=xpost, keep_all_points=False) if not updated: - newMesh = None + new_mesh = None if B._currentIncumbentInf: B._currentIncumbentInf.mesh.refineDeltaFrameSize() - newMesh = copy.deepcopy(B._currentIncumbentFeas.mesh) if B._currentIncumbentFeas else copy.deepcopy(B._currentIncumbentInf.mesh) if B._currentIncumbentInf else None + new_mesh = copy.deepcopy(B._currentIncumbentFeas.mesh) if B._currentIncumbentFeas else copy.deepcopy(B._currentIncumbentInf.mesh) if B._currentIncumbentInf else None B.updateCurrentIncumbents() if search.sampling_t != SAMPLING_METHOD.ACTIVE.name: search.update_local_region(region="contract") if B._currentIncumbentFeas: B._currentIncumbentFeas.mesh.refineDeltaFrameSize() - newMesh = copy.deepcopy(B._currentIncumbentFeas.mesh) if B._currentIncumbentFeas else copy.deepcopy(B._currentIncumbentInf.mesh) if B._currentIncumbentInf else None + new_mesh = copy.deepcopy(B._currentIncumbentFeas.mesh) if B._currentIncumbentFeas else copy.deepcopy(B._currentIncumbentInf.mesh) if B._currentIncumbentInf else None B.updateCurrentIncumbents() if search.sampling_t != SAMPLING_METHOD.ACTIVE.name: search.update_local_region(region="contract") - if newMesh: - search.mesh = newMesh + if new_mesh: + search.mesh = new_mesh else: search.mesh.refineDeltaFrameSize() if search.sampling_t != SAMPLING_METHOD.ACTIVE.name: search.update_local_region(region="contract") else: - search.mesh = copy.deepcopy(B._currentIncumbentFeas.mesh) if updatedF else copy.deepcopy(B._currentIncumbentInf.mesh) if updatedInf else search.mesh - search.xmin = copy.deepcopy(B._currentIncumbentFeas) if updatedF else copy.deepcopy(B._currentIncumbentInf) if updatedInf else search.xmin + search.mesh = copy.deepcopy(B._currentIncumbentFeas.mesh) if updated_f else copy.deepcopy(B._currentIncumbentInf.mesh) if updated_inf else search.mesh + search.xmin = copy.deepcopy(B._currentIncumbentFeas) if updated_f else copy.deepcopy(B._currentIncumbentInf) if updated_inf else search.xmin if search.sampling_t != SAMPLING_METHOD.ACTIVE.name: search.update_local_region(region="expand") for i in range(len(xpost)): post.poll_dirs.append(xpost[i]) - search.hashtable.best_hash_ID = [] + search.hashtable.best_hash_id = [] search.hashtable.add_to_best_cache(B.getAllPoints()) - post.xmin = B._currentIncumbentFeas if updatedF else B._currentIncumbentInf if updatedInf else search.xmin + post.xmin = B._currentIncumbentFeas if updated_f else B._currentIncumbentInf if updated_inf else search.xmin search.mesh.update() if iteration == 1: @@ -278,35 +241,45 @@ def main(*args) -> Dict[str, Any]: if options.save_results: post.nd_points = [] - post.output_results(out=out, allRes=False) + post.output_results(out=out, all_res=False) if param.isPareto: for i in range(len(B.getAllPoints())): post.nd_points.append(B.getAllPoints()[i]) - post.output_nd_results(outP) + post.output_nd_results(out_p) - if log is not None: + if log: log.log_msg(msg=post.__str__(), msg_type=MSG_TYPE.INFO) if options.display: print(post) - Failure_check = iteration > 0 and search.Failure_stop is not None and search.Failure_stop and not (search.success != SUCCESS_TYPES.FS or SUCCESS_TYPES.PS) + failure_check = iteration > 0 and search.Failure_stop is not None and search.Failure_stop and not (search.success != SUCCESS_TYPES.FS or SUCCESS_TYPES.PS) if search.bb_handle.bb_eval - bbevalold <= 0: num_strat += 1 if num_strat > 5: - search.terminate = True - if (Failure_check or search.bb_handle.bb_eval >= options.budget) or (all(abs(search.mesh.getdeltaMeshSize().coordinates[pp]) < options.tol for pp in range(search.mesh._n)) or search.bb_handle.bb_eval >= options.budget or search.terminate): - log.log_msg(f"\n--------------- Termination of the search step ---------------", MSG_TYPE.INFO) + search.exploreNew = True + num_strat = 0 + else: + num_strat = 0 + if (failure_check or search.bb_handle.bb_eval >= options.budget) or (all(abs(search.mesh.getdeltaMeshSize().coordinates[pp]) < options.tol for pp in range(search.mesh._n)) or search.bb_handle.bb_eval >= options.budget or search.terminate): + log.log_msg("\n--------------- Termination of the search step ---------------", MSG_TYPE.INFO) if (all(abs(search.mesh.getdeltaMeshSize().coordinates[pp]) < options.tol for pp in range(search.mesh._n))): log.log_msg("Termination criterion hit: the mesh size is below the minimum threshold defined.", MSG_TYPE.INFO) if (search.bb_handle.bb_eval >= options.budget or search.terminate): log.log_msg("Termination criterion hit: evaluation budget is exhausted.", MSG_TYPE.INFO) - if (Failure_check): - log.log_msg(f"Termination criterion hit (optional): failed to find a successful point in iteration # {iteration}.", MSG_TYPE.INFO) - log.log_msg(f"-----------------------------------------------------------------\n", MSG_TYPE.INFO) + if (failure_check): + log.log_msg("Termination criterion hit (optional): failed to find a successful point in iteration # {iteration}.", MSG_TYPE.INFO) + log.log_msg("-----------------------------------------------------------------\n", MSG_TYPE.INFO) break iteration += 1 toc = time.perf_counter() + if isinstance(B, BarrierMO): + rp: Optional[CandidatePoint] = None + if param.ref_point: + rp = Point() + rp.coordinates = param.ref_point + perf_m = Metrics(nd_solutions=B.getAllPoints(), nobj=B._nobj, ref_point=rp) + HV = perf_m.hypervolume() """ If benchmarking, then populate the results in the benchmarking output report """ if importlib.util.find_spec('BMDFO') and len(args) > 1 and isinstance(args[1], toy.Run): @@ -330,24 +303,24 @@ def main(*args) -> Dict[str, Any]: print(f"{search.bb_handle.blackbox}: fmin = {search.xmin.f} , hmin= {search.xmin.h:.2f}") elif importlib.util.find_spec('BMDFO') and len(args) > 1 and not isinstance(args[1], toy.Run): - if log is not None: + if log: log.log_msg(msg="Could not find " + args[1] + " in the internal BM suite.", msg_type=MSG_TYPE.ERROR) raise IOError("Could not find " + args[1] + " in the internal BM suite.") if options.display: - if log is not None: + if log: log.log_msg(" end of orthogonal MADS ", MSG_TYPE.INFO) print(" end of orthogonal MADS ") - if log is not None: + if log: log.log_msg(" Final objective value: " + str(search.xmin.f) + ", hmin= " + str(search.xmin.h), MSG_TYPE.INFO) print(" Final objective value: " + str(search.xmin.f) + ", hmin= " + str(search.xmin.h)) if options.save_coordinates: post.output_coordinates(out) - if log is not None: + if log: log.log_msg("\n ---Run Summary---", MSG_TYPE.INFO) log.log_msg(f" Run completed in {toc - tic:.4f} seconds", MSG_TYPE.INFO) log.log_msg(msg=f" # of successful search steps = {search.n_successes}", msg_type=MSG_TYPE.INFO) @@ -392,129 +365,11 @@ def main(*args) -> Dict[str, Any]: "psize": search.mesh.getDeltaFrameSize().coordinates, "psuccess": search.xmin.mesh.getDeltaFrameSize().coordinates, # "pmax": search.mesh.psize_max, - "msize": search.mesh.getdeltaMeshSize().coordinates} - - if search.visualize: - sc_old = search.store_cache - cc_old = search.check_cache - search.check_cache = False - search.store_cache = False - temp = CandidatePoint() - temp.coordinates = output["xmin"] - for ii in range(len(ax)): - for jj in range(len(xmin.coordinates)): - for kk in range(jj+1, len(xmin.coordinates)): - if kk != jj: - ps = visualize(xinput, jj, kk, search.mesh.getdeltaMeshSize().coordinates, vv, fig, ax, temp, ps, bbeval=search.bb_handle, lb=search.prob_params.lb, ub=search.prob_params.ub, title=search.prob_params.problem_name, blk=True,vnames=search.prob_params.var_names, spindex=ii, bestKnown=search.prob_params.best_known) - search.check_cache = sc_old - search.store_cache = cc_old + "msize": search.mesh.getdeltaMeshSize().coordinates, + "HV": HV if param.isPareto else "NA"} return output, search - - -def visualize(points: List[CandidatePoint], hc_index, vc_index, msize, vlim, fig, axes, pmin, ps = None, title="unknown", blk=False, vnames=None, bbeval=None, lb = None, ub=None, spindex=0, bestKnown=None): - - x: np.ndarray = np.zeros(len(points)) - y: np.ndarray = np.zeros(len(points)) - - for i in range(len(points)): - x[i] = points[i].coordinates[hc_index] - y[i] = points[i].coordinates[vc_index] - xmin = pmin.coordinates[hc_index] - ymin = pmin.coordinates[vc_index] - - # Plot grid's dynamic updates - # nrx = int((vlim[hc_index, 1] - vlim[hc_index, 0])/msize) - # nry = int((vlim[vc_index, 1] - vlim[vc_index, 0])/msize) - - # minor_ticksx=np.linspace(vlim[hc_index, 0],vlim[hc_index, 1],nrx+1) - # minor_ticksy=np.linspace(vlim[vc_index, 0],vlim[vc_index, 1],nry+1) - isFirst = False - - if ps[spindex] == None: - isFirst = True - ps[spindex] =[] - if bbeval is not None and lb is not None and ub is not None: - xx = np.arange(lb[hc_index], ub[hc_index], 0.1) - yy = np.arange(lb[vc_index], ub[vc_index], 0.1) - X, Y = np.meshgrid(xx, yy) - Z = np.zeros_like(X) - for i in range(X.shape[0]): - for j in range(X.shape[1]): - Z[i,j] = bbeval.eval([X[i,j], Y[i,j]])[0] - bbeval.bb_eval -= 1 - if bestKnown is not None: - best_index = np.argwhere(Z <= bestKnown+0.005) - if best_index.size == 0: - best_index = np.argwhere(Z == np.min(Z)) - xbk = X[best_index[0][0], best_index[0][1]] - ybk = Y[best_index[0][0], best_index[0][1]] - temp1 = axes[spindex].contourf(X, Y, Z, 100) - axes[spindex].set_aspect('equal') - fig.subplots_adjust(right=0.8) - cbar_ax = fig.add_axes([0.85, 0.1, 0.01, 0.85]) - fig.colorbar(temp1, cbar_ax) - fig.suptitle(title) - - ps[spindex].append(temp1) - - - - temp2, = axes[spindex].plot(xmin, ymin, 'ok', alpha=0.08, markersize=2) - - ps[spindex].append(temp2) - - if bestKnown is not None: - temp3, = axes[spindex].plot(xbk, ybk, 'dr', markersize=2) - ps[spindex].append(temp3) - - - - else: - ps[spindex][1].set_xdata(x) - ps[spindex][1].set_ydata(y) - - - - fig.canvas.draw() - fig.canvas.flush_events() - # axes.set_xticks(minor_ticksx,major=True) - # axes.set_yticks(minor_ticksy,major=True) - - # axes.grid(which="major",alpha=0.3) - # ps[1].set_xdata(x) - # ps[1].set_ydata(y) - if blk: - if bestKnown is not None: - t1 = ps[spindex][2] - t2, =axes[spindex].plot(x, y, 'ok', alpha=0.08, markersize=2) - - - t3, = axes[spindex].plot(xmin, ymin, '*b', markersize=4) - if bestKnown is not None: - fig.legend((t1, t2, t3), ("best_known", "sample_points", "best_found")) - else: - fig.legend((t2, t3), ("sample_points", "best_found")) - else: - axes[spindex].plot(x, y, 'ok', alpha=0.08, markersize=2) - # axes[spindex].plot(xmin, ymin, '*b', markersize=4) - if vnames is not None: - axes[spindex].set_xlabel(vnames[hc_index]) - axes[spindex].set_ylabel(vnames[vc_index]) - if lb is not None and ub is not None: - axes[spindex].set_xlim([lb[hc_index], ub[hc_index]]) - axes[spindex].set_ylim([lb[vc_index], ub[vc_index]]) - plt.show(block=blk) - - if blk: - fig.savefig(f"{title}.png", bbox_inches='tight') - plt.close(fig) - - return ps - - - def rosen(x, *argv): x = np.asarray(x) y = [np.sum(100.0 * (x[1:] - x[:-1] ** 2.0) ** 2.0 + (1 - x[:-1]) ** 2.0, @@ -525,7 +380,7 @@ def rosen(x, *argv): def test_omads_callable_quick(): - eval = {"blackbox": rosen} + eval_func = {"blackbox": rosen} param = {"baseline": [-2.0, -2.0], "lb": [-5, -5], "ub": [10, 10], @@ -540,13 +395,12 @@ def test_omads_callable_quick(): } options = {"seed": 0, "budget": 100000, "tol": 1e-12, "display": True} - data = {"evaluator": eval, "param": param, "options": options, "sampling": sampling} + data = {"evaluator": eval_func, "param": param, "options": options, "sampling": sampling} out: Dict = main(data) print(out) -def test_omads_file_quick(): - file = "tests\\bm\\constrained\\sphere.json" + if __name__ == "__main__": freeze_support() diff --git a/src/OMADS/_common.py b/src/OMADS/_common.py index f962b4d..0c1ad22 100644 --- a/src/OMADS/_common.py +++ b/src/OMADS/_common.py @@ -22,26 +22,23 @@ # Copyright (C) 2022 Ahmed H. Bayoumy # # ------------------------------------------------------------------------------------# -import copy -from dataclasses import dataclass, field -import importlib +from dataclasses import dataclass import logging -import operator import time import shutil import os -from typing import List, Dict, Any import numpy as np -from .CandidatePoint import CandidatePoint import json -from ._globals import * +from ._globals import MSG_TYPE +import pkg_resources + np.set_printoptions(legacy='1.21') @dataclass class validator: - def checkInputFile(self, args) -> dict: + def check_input_file(self, args) -> dict: if type(args[0]) is dict: data = args[0] elif isinstance(args[0], str): @@ -67,11 +64,11 @@ def checkInputFile(self, args) -> dict: @dataclass class logger: log: None = None - isVerbose: bool = False + is_verbose: bool = False - def initialize(self, file: str, wTime = False, isVerbose = False): + def initialize(self, file: str, w_time = False, is_verbose = False): # Create and configure logger - self.isVerbose = isVerbose + self.is_verbose = is_verbose logging.basicConfig(filename=file, format='%(message)s', filemode='w') @@ -82,8 +79,8 @@ def initialize(self, file: str, wTime = False, isVerbose = False): #Now we are going to Set the threshold of logger to DEBUG self.log.setLevel(logging.DEBUG) cur_time = time.strftime("%Y-%m-%d, %H:%M:%S", time.localtime()) - self.log_msg(msg=f"###################################################### \n", msg_type=MSG_TYPE.INFO) - self.log_msg(msg=f"################# OMADS ver. 2401 #################### \n", msg_type=MSG_TYPE.INFO) + self.log_msg(msg="###################################################### \n", msg_type=MSG_TYPE.INFO) + self.log_msg(msg=f"################# OMADS release #{2410} #################### \n", msg_type=MSG_TYPE.INFO) self.log_msg(msg=f"############### {cur_time} ################# \n", msg_type=MSG_TYPE.INFO) # Remove all handlers associated with the root logger object. @@ -91,7 +88,7 @@ def initialize(self, file: str, wTime = False, isVerbose = False): logging.root.removeHandler(handler) # Create and configure logger - if wTime: + if w_time: logging.basicConfig(filename=file, format='%(asctime)s %(message)s', filemode='a') @@ -119,16 +116,16 @@ def log_msg(self, msg: str, msg_type: MSG_TYPE): elif msg_type == MSG_TYPE.CRITICAL: self.log.critical(msg) - def relocate_logger(self, source_file: str = None, Dest_file: str = None): - if Dest_file is not None and source_file is not None and os.path.exists(source_file): - shutil.copy(source_file, Dest_file) + def relocate_logger(self, source_file: str = None, dest_file: str = None): + if dest_file is not None and source_file is not None and os.path.exists(source_file): + shutil.copy(source_file, dest_file) if os.path.exists("DSMToDMDO.yaml"): - shutil.copy("DSMToDMDO.yaml", Dest_file) + shutil.copy("DSMToDMDO.yaml", dest_file) # Remove all handlers associated with the root logger object. for handler in logging.root.handlers[:]: logging.root.removeHandler(handler) # Create and configure logger - logging.basicConfig(filename=os.path.join(Dest_file, "DMDO.log"), + logging.basicConfig(filename=os.path.join(dest_file, "DMDO.log"), format='%(asctime)s %(message)s', filemode='a') #Let us Create an object diff --git a/src/OMADS/_globals.py b/src/OMADS/_globals.py index 311ebce..923c346 100644 --- a/src/OMADS/_globals.py +++ b/src/OMADS/_globals.py @@ -27,7 +27,6 @@ import warnings import numpy as np import platform -import pandas as pd np.set_printoptions(legacy='1.21') @@ -77,7 +76,6 @@ def precision(self): def precision(self, val: str): self._prec = val self._prec = val - isWin = platform.platform().split('-')[0] == 'Windows' if val == "high": if (not hasattr(np, 'float128')): 'Warning: MS Windows does not support precision with the {1e-18} high resolution of the python numerical library (numpy) so high precision will be changed to medium precision which supports {1e-15} resolution check: https://numpy.org/doc/stable/user/basics.types.html ' diff --git a/tests/OMADS_MO_BASIC.py b/tests/OMADS_MO_BASIC.py deleted file mode 100644 index d12d338..0000000 --- a/tests/OMADS_MO_BASIC.py +++ /dev/null @@ -1,391 +0,0 @@ -from OMADS import POLL, SEARCH, MADS -import copy -import os -import numpy as np - -from typing import Dict, List -from multiprocessing import freeze_support -import platform - -def common_dict(): - outDict: dict = { - "evaluator": - { - "blackbox": None}, - - "param": - { - "baseline": None, - "lb": None, - "ub": None, - "var_names": ["x", "y"], - "fun_names": ["f1", "f2"], - # "constraints_type": ["PB", "PB"], - "nobj": 2, - "isPareto": True, - "scaling": None, - "LAMBDA": [1E5, 1E5], - "RHO": 1.0, - "h_max": np.inf, - "meshType": "GMESH", - "post_dir": None - }, - - "options": - { - "seed": 0, - "budget": 2000, - "tol": 1e-12, - "psize_init": 1, - "display": False, - "opportunistic": False, - "check_cache": True, - "store_cache": True, - "collect_y": False, - "rich_direction": True, - "precision": "high", - "save_results": True, - "save_coordinates": False, - "save_all_best": False, - "parallel_mode": False - }, - - "search": { - "type": "sampling", - "s_method": "ACTIVE", - "ns": 10, - "visualize": False - }, - } - return outDict - -def MO_Binh_and_Korn(x): - f1 = 4 * x[0]**2 + 4 * x[1]**2 - f2 = (x[0] - 5)**2 + (x[1] - 5)**2 - g1 = (x[0]-5)**2 + x[1]**2 -25 - g2 = 7.7 - (x[0]-8)**2 - (x[1]+3)**2 - - return [[f1, f2], [g1, g2]] - -def MO_Chankong_and_Haimes(x): - f1 = 2 + (x[0]-2)**2 + (x[1]-1)**2 - f2 = 9*x[0]-(x[1]-1)**2 - g1 = x[0]**2 + x[1]**2-225 - g2 = x[0] -3*x[1]+10 - - return [[f1, f2], [g1, g2]] - -def MO_Test_function_4(x): - f1 = x[0]**2-x[1] - f2 = -0.5*x[0]-x[1]-1 - g1 = -(6.5 - (x[0]/6) - x[1]) - g2 = -(7.5 - 0.5 *x[0] -x[1]) - g3 = -(30 - 5*x[0] -x[1]) - - return [[f1, f2], [g1, g2, g3]] - -def MO_Kursawe(x): - f1 = sum([-10*np.exp(-0.2*np.sqrt(x[i]**2 + x[i+1]**2)) for i in range(2)]) - f2 = sum([abs(x[i])**0.8 + 5*np.sin(x[i]**3) for i in range(3)]) - - return [[f1, f2], [0]] - -def MO_Fonseca_Fleming(x): - n = len(x) - f1 = 1 - np.exp(-sum([(x[i]-(1/np.sqrt(n)))**2 for i in range(n)])) - f2 = 1 - np.exp(-sum([(x[i]+(1/np.sqrt(n)))**2 for i in range(n)])) - - return [[f1, f2], [0]] - -def MO_Osyczka_Kundu(x): - f1 = -25*(x[0]-2)**2 - (x[1]-2)**2 - (x[2]-1)**2 - (x[3]-4)**2 - (x[4]-1)**2 - f2 = sum([x[i]**2 for i in range(6)]) - - g1 = x[0] + x[1] -2 - g2 = 6 - x[0] - x[1] - g3 = 2 - x[1] + x[0] - g4 = 2 - x[0] + 3*x[1] - g5 = 4-(x[2]-3)**2 -x[3] - g6 = (x[4]-3)**2 + x[5] -4 - - return [[f1, f2], [-g1, -g2, -g3, -g4, -g5, -g6]] - -def MO_CTP1(x): - f1 = x[0] - f2 = (1+x[1])*np.exp(-(x[0])/(1+x[1])) - g1 = 1-((f2)/(0.858*np.exp(-0.541*f1))) - g2 = 1-(f2/(0.728*np.exp(-0.295*f1))) - - return [[f1, f2], [g1, g2]] - -def MO_Ex(x): - f1 = x[0] - f2 = (1+x[1])/x[0] - - g1 = 6-(x[1]+9*x[0]) - g2 = 1+x[1] - 9*x[0] - - return [[f1,f2],[g1,g2]] - -def MO_ZDT1(x): - f1 = x[0] # objective 1 - g = 1 + 9 * np.sum(np.divide(x[1:len(x)], (len(x) - 1))) - h = 1 - np.sqrt(f1 / g) - f2 = g * h # objective 2 - - return [[f1, f2], [0]] - -def MO_ZDT3(x): - f1 = x[0] # objective 1 - g = 1 + (9/(len(x) - 1)) * np.sum(x[1:len(x)]) - h = 1 - np.sqrt(f1 / g) - (f1/g)*np.sin(10*np.pi*f1) - f2 = g * h # objective 2 - - return [[f1, f2], [0]] - -def MO_ZDT4(x): - f1 = x[0] # objective 1 - g = 1 + 10*(len(x)-1) + np.sum([x[i]**2 - 10*np.cos(4*np.pi*x[i]) for i in range(1, len(x))]) - h = 1 - np.sqrt(f1 / g) - f2 = g * h # objective 2 - - return [[f1, f2], [0]] - -def MO_ZDT6(x): - f1 = 1 - np.exp(-4*x[0]) * np.sin(6*np.pi*x[0])**6 - g = 1+9*(sum(x[1:len(x)])/9)**.25 - h = 1 - (f1/g)**2 - f2 = g * h # objective 2 - - return [[f1, f2], [0]] - -def test_MO_Binh_and_Korn(): - data = common_dict() - data["evaluator"]["blackbox"] = MO_Binh_and_Korn - data["param"]["name"] = "Binh_and_Korn" - data["param"]["baseline"] = [0, 0] - data["param"]["lb"] = [0, 0] - data["param"]["ub"] = [5, 3] - data["param"]["constraints_type"] = ["PB", "PB"] - data["param"]["scaling"] = [5, 3] - data["param"]["post_dir"] = "./tests/bm/MOO/constrained/Binh_and_Korn/post" - - # POLL.main(data) - # SEARCH.main(data) - MADS.main(data) - -def test_MO_Chankong_and_Haimes(): - data = common_dict() - data["evaluator"]["blackbox"] = MO_Chankong_and_Haimes - data["param"]["name"] = "Chankong_and_Haimes" - data["param"]["baseline"] = [0, 0] - data["param"]["lb"] = [-20, -20] - data["param"]["ub"] = [20, 20] - data["param"]["constraints_type"] = ["PB", "PB"] - data["param"]["scaling"] = [40, 40] - data["param"]["post_dir"] = "./tests/bm/MOO/constrained/Chankong_and_Haimes/post" - - # POLL.main(data) - # SEARCH.main(data) - MADS.main(data) - -def test_MO_Fonseca_Fleming(): - data = common_dict() - data["evaluator"]["blackbox"] = MO_Fonseca_Fleming - data["param"]["name"] = "Fonseca_Fleming" - data["param"]["baseline"] = [0, 0] - data["param"]["lb"] = [-4, -4] - data["param"]["ub"] = [4, 4] - data["meshType"] = "GMESH" - # data["param"]["constraints_type"] = ["EB"] - data["param"]["scaling"] = [8, 8] - data["param"]["post_dir"] = "./tests/bm/MOO/unconstrained/Fonseca_Fleming/post" - data["options"]["budget"] = 1000 - - # POLL.main(data) - # SEARCH.main(data) - MADS.main(data) - -def test_MO_Test_function_4(): - data = common_dict() - data["evaluator"]["blackbox"] = MO_Test_function_4 - data["param"]["name"] = "Test_function_4" - data["param"]["baseline"] = [0, 0]#[3, 3] - data["param"]["lb"] = [-7, -7] - data["param"]["ub"] = [4, 4] - data["meshType"] = "GMESH" - data["param"]["constraints_type"] = ["PB", "PB"] - data["param"]["scaling"] = [10, 10] - data["param"]["post_dir"] = "./tests/bm/MOO/constrained/Test_function_4/post" - data["options"]["budget"] = 1000 - - # data["search"]["type"] = "VNS" - # data["search"]["s_method"] = "RANDOM" - # data["search"]["ns"] = 10 - - # POLL.main(data) - # SEARCH.main(data) - MADS.main(data) - -def test_MO_Kursawe(): - # TODO: uncon logic needs review - data = common_dict() - data["evaluator"]["blackbox"] = MO_Kursawe - data["param"]["name"] = "Kursawe" - # data["param"]["baseline"] = [-2.0, 0.5, -4.5] - data["param"]["baseline"] = [-2.0, -0.5, -5] - data["param"]["var_names"] = ['x1', 'x2', 'x3'] - data["param"]["lb"] = [-5, -5, -5] - data["param"]["ub"] = [5, 5, 5] - # data["param"]["LAMBDA"]= None - # data["param"]["RHO"] = 1 - # data["param"]["h_max"] = 0 - data["meshType"] = "GMESH" - # data["param"]["constraints_type"] = ["PB"] - data["param"]["scaling"] = [10, 10, 10] - data["param"]["post_dir"] = "./tests/bm/MOO/unconstrained/Kursawe/post" - data["options"]["budget"] = 10000 - - # POLL.main(data) - # SEARCH.main(data) - MADS.main(data) - -def test_MO_Osyczka_Kundu(): - # COMPLETED: Investigate why starting from infeasible point does not work in MOO - data = common_dict() - data["evaluator"]["blackbox"] = MO_Osyczka_Kundu - data["param"]["name"] = "Osyczka_Kundu" - data["param"]["baseline"] = [3, 2, 2, 0, 5, 10] - # data["param"]["baseline"] = [5, 1, 5, 0, 5, 8] - data["param"]["var_names"] = ['x1', 'x2', 'x3', 'x4', 'x5', 'x6'] - data["param"]["lb"] = [0, 0, 1, 0, 1, 0] - data["param"]["ub"] = [10, 10, 5, 6, 5, 10] - data["param"]["meshType"] = "GMESH" - data["param"]["constraints_type"] = ["PB"]*6 - data["param"]["scaling"] = [10, 10, 4, 6, 4, 10] - data["param"]["post_dir"] = "./tests/bm/MOO/constrained/Osyczka_Kundu/post" - data["options"]["budget"] = 30000 - data["options"]["seed"] = 1234 - - # POLL.main(data) - data["search"]["ns"] = 22 - # SEARCH.main(data) - MADS.main(data) - -def test_MO_CTP1(): - data = common_dict() - data["evaluator"]["blackbox"] = MO_CTP1 - data["param"]["name"] = "MO_CTP1" - data["param"]["baseline"] = [0.5, 0.5] - data["param"]["var_names"] = ['x1', 'x2'] - data["param"]["lb"] = [0, 0] - data["param"]["ub"] = [1, 1] - data["param"]["meshType"] = "GMESH" - data["param"]["constraints_type"] = ["PB"]*2 - data["param"]["scaling"] = [1, 1] - data["param"]["post_dir"] = "./tests/bm/MOO/constrained/MO_CTP1/post" - data["options"]["budget"] = 3000 - data["search"]["ns"] = 50 - # POLL.main(data) - # SEARCH.main(data) - MADS.main(data) - -def test_MO_Ex(): - data = common_dict() - data["evaluator"]["blackbox"] = MO_Ex - data["param"]["name"] = "Ex" - data["param"]["baseline"] = [0.6, 2.5] - data["param"]["var_names"] = ['x1', 'x2'] - data["param"]["lb"] = [0.1, 0] - data["param"]["ub"] = [1, 5] - data["param"]["meshType"] = "GMESH" - data["param"]["constraints_type"] = ["PB"]*2 - data["param"]["scaling"] = [0.9, 5] - data["param"]["post_dir"] = "./tests/bm/MOO/constrained/Ex/post" - data["options"]["budget"] = 5000 - data["search"]["ns"] = 15 - # POLL.main(data) - # SEARCH.main(data) - MADS.main(data) - -def test_MO_ZDT1(): - d = 30 - data = common_dict() - data["evaluator"]["blackbox"] = MO_ZDT1 - data["param"]["name"] = "MO_ZDT1" - np.random.seed(seed= 12345) - data["param"]["baseline"] = np.random.rand(d) - data["param"]["var_names"] = [f'x{i}' for i in range(d)] - data["param"]["lb"] = [0]*d - data["param"]["ub"] = [1]*d - data["param"]["meshType"] = "GMESH" - data["param"]["constraints_type"] = ["PB"] - data["param"]["scaling"] = [1]*d - data["param"]["post_dir"] = "./tests/bm/MOO/unconstrained/MO_ZDT1/post" - data["options"]["budget"] = 10000 - # POLL.main(data) - # SEARCH.main(data) - MADS.main(data) - -def test_MO_ZDT3(): - d = 30 - data = common_dict() - data["evaluator"]["blackbox"] = MO_ZDT3 - data["param"]["name"] = "MO_ZDT3" - np.random.seed(seed= 12345) - data["param"]["baseline"] = np.random.rand(d) - data["param"]["var_names"] = [f'x{i}' for i in range(d)] - data["param"]["lb"] = [0]*d - data["param"]["ub"] = [1]*d - data["param"]["meshType"] = "GMESH" - data["param"]["constraints_type"] = ["PB"] - data["param"]["scaling"] = [1]*d - data["param"]["post_dir"] = "./tests/bm/MOO/unconstrained/MO_ZDT3/post" - data["options"]["budget"] = 10000 - data["search"]["ns"] = 50 - # POLL.main(data) - # SEARCH.main(data) - MADS.main(data) - -def test_MO_ZDT4(): - d = 10 - data = common_dict() - data["evaluator"]["blackbox"] = MO_ZDT4 - data["param"]["name"] = "MO_ZDT4" - np.random.seed(seed= 12345) - data["param"]["baseline"] = np.random.rand(1).tolist() + np.random.uniform(low=-10, high=10, size=(d-1,)).tolist() - data["param"]["var_names"] = [f'x{i}' for i in range(d)] - data["param"]["lb"] = [0] + [-10]*(d-1) - data["param"]["ub"] = [1] + [10]*(d-1) - data["param"]["meshType"] = "GMESH" - data["param"]["constraints_type"] = ["PB"] - data["param"]["scaling"] = [1] + [20]*(d-1) - data["param"]["post_dir"] = "./tests/bm/MOO/unconstrained/MO_ZDT4/post" - data["options"]["budget"] = 5000 #40000 - data["search"]["ns"] = 55 - # POLL.main(data) - # SEARCH.main(data) - MADS.main(data) - -def test_MO_ZDT6(): - d = 10 - data = common_dict() - data["evaluator"]["blackbox"] = MO_ZDT6 - data["param"]["name"] = "MO_ZDT6" - np.random.seed(seed= 12345) - data["param"]["baseline"] = np.random.rand(d) - data["param"]["var_names"] = [f'x{i}' for i in range(d)] - data["param"]["lb"] = [0]*d - data["param"]["ub"] = [1]*d - data["param"]["meshType"] = "OMESH" - data["param"]["constraints_type"] = ["PB"] - data["param"]["scaling"] = [1]*d - data["param"]["post_dir"] = "./tests/bm/MOO/unconstrained/MO_ZDT6/post" - data["options"]["budget"] = 10000 - data["search"]["ns"] = 100 - # POLL.main(data) - # SEARCH.main(data) - MADS.main(data) - -if __name__ == "__main__": - freeze_support() diff --git a/tests/test_OMADS_BASIC.py b/tests/test_OMADS_BASIC.py index a7dc801..7664565 100644 --- a/tests/test_OMADS_BASIC.py +++ b/tests/test_OMADS_BASIC.py @@ -1,4 +1,5 @@ import importlib +import time from OMADS import POLL, SEARCH, MADS from matplotlib import pyplot as plt import copy @@ -9,6 +10,41 @@ from multiprocessing import freeze_support import platform +import logging + +# Configure the logging +# Create a custom logger + + +logger = logging.getLogger('OMADS_SO_BBO_unit_tests') +logger.setLevel(logging.DEBUG) # Set to DEBUG to capture all messages + +# Create a console handler +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.INFO) # Only log INFO and above to console + +# Create a file handler +file_handler = logging.FileHandler(filename='tests/OMADS_BBO_unit_test.log', mode = 'a') +file_handler.setLevel(logging.DEBUG) # Log all messages to file + +# Create a formatter and set it for handlers +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +console_handler.setFormatter(formatter) +file_handler.setFormatter(formatter) + +# Add handlers to the logger +logger.addHandler(console_handler) +logger.addHandler(file_handler) + +# Example filter to exclude messages from the root logger +class NoRootMessagesFilter(logging.Filter): + def filter(self, record): + return record.name != 'root' + +# Add the filter to handlers +console_handler.addFilter(NoRootMessagesFilter()) +file_handler.addFilter(NoRootMessagesFilter()) + def geom_prog(x, *argv): xx = x x2 = np.sqrt(xx[3] ** 2 + xx[4] ** -2 + xx[5] ** -2 + xx[6] ** 2) @@ -35,9 +71,15 @@ def thin_con(x): y = [[f], [c1, c2]] return y -def test_MADS_callable_quick_2d(): +def test_create_out_file(): + open('tests/OMADS_BBO_unit_test.log', 'w').close() + + +def test_callable_quick_2d(): + logger.info('\nStarted running bbo_2d_rosenbrock test... \n') + tic = time.perf_counter() d = 2 - eval = {"blackbox": rosen} + eval_callable = {"blackbox": rosen} param = {"name": "RB","baseline": [-2.5]*d, "lb": [-5]*d, "ub": [10]*d, @@ -50,45 +92,98 @@ def test_MADS_callable_quick_2d(): "visualize": False, "criterion": None } - options = {"seed": 10000, "budget": 3000, "tol": 1e-9, "display": False, "check_cache": True, "store_cache": True, "rich_direction": True, "opportunistic": False, "save_results": False, "isVerbose": False} + options = {"seed": 10000, "budget": 1100, "tol": 1e-9, "display": False, "check_cache": True, "store_cache": True, "rich_direction": True, "opportunistic": False, "save_results": False, "isVerbose": False} search = { "type": "sampling", "s_method": "ACTIVE", "ns": int((d+1)*(d+2)/2)+55, "visualize": False } - data = {"evaluator": eval, "param": param, "options": options, "sampling": sampling, "search": search} - - outM: Dict = MADS.main(data) - outP: Dict = POLL.main(data) - outS: Dict = SEARCH.main(data) - OMS = outM[0]["fmin"][0] - OPS = outP[0]["fmin"][0] - OSS = outS[0]["fmin"][0] - if (outM[0]["fmin"][0] > 0.0006): - raise ValueError(f"\nSequential Exec: MADS: fmin: {OMS} > {0.0006} \nPoll: fmin = {OPS}\nSearch: fmin = {OSS}") + data = {"evaluator": eval_callable, "param": param, "options": options, "sampling": sampling, "search": search} + data["param"]["lhs_search_initialization"] = True + logger.info('\nStarted running MADS on bbo_2d_rosenbrock serial exectution ...') + ticms = time.perf_counter() + out_mads: Dict = MADS.main(data) + tocms = time.perf_counter() + logger.info(f'Completed serial MADS run on bbo_2d_rosenbrock in {tocms - ticms:.4f} seconds.\n') + + ticps = time.perf_counter() + logger.info('\nStarted running POLL on bbo_2d_rosenbrock serial exectution ...') + out_poll: Dict = POLL.main(data) + tocps = time.perf_counter() + logger.info(f'Completed serial POLL run on bbo_2d_rosenbrock in {tocps - ticps:.4f} seconds.\n') + + ticss = time.perf_counter() + logger.info('\nStarted running SEARCH on bbo_2d_rosenbrock serial exectution ...') + out_search: Dict = SEARCH.main(data) + tocss = time.perf_counter() + logger.info(f'Completed serial SEARCH run on bbo_2d_rosenbrock in {tocss - ticss:.4f} seconds.\n') + + OMS = out_mads[0]["fmin"][0] + OPS = out_poll[0]["fmin"][0] + OSS = out_search[0]["fmin"][0] - if (outP[0]["fmin"][0] > 0.0006): - raise ValueError(f"\nSequential Exec: POLL: fmin: {OPS} > {0.0006} \nMADS: fmin = {OMS}\nSearch: fmin = {OSS}") - if (outS[0]["fmin"][0] > 0.0006): - raise ValueError(f"\nSequential Exec: Search: fmin {OSS} > {0.0006} \nMADS: fmin = {OMS}\nPoll: fmin = {OPS}") data["options"]["parallel_mode"] = True - OMS = outM[0]["fmin"][0] - OPS = outP[0]["fmin"][0] - OSS = outS[0]["fmin"][0] - if (outM[0]["fmin"][0] > 0.0006): - raise ValueError(f"\nParallel Exec: MADS: fmin: {OMS} > {0.0006} \nPoll: fmin = {OPS}\nSearch: fmin = {OSS}") + data["options"]["np"] = 4 + logger.info('\nStarted running MADS on bbo_2d_rosenbrock parallel exectution ...') + ticmp = time.perf_counter() + out_mads: Dict = MADS.main(data) + tocmp = time.perf_counter() + logger.info(f'Completed parallel MADS run on bbo_2d_rosenbrock in {tocmp - ticmp:.4f} seconds.\n') + + ticpp = time.perf_counter() + logger.info('\nStarted running POLL on bbo_2d_rosenbrock parallel exectution ...') + out_poll: Dict = POLL.main(data) + tocpp = time.perf_counter() + logger.info(f'Completed parallel POLL run on bbo_2d_rosenbrock in {tocpp - ticpp:.4f} seconds.\n') + + ticsp = time.perf_counter() + logger.info('\nStarted running SEARCH on bbo_2d_rosenbrock parallel exectution ...') + out_search: Dict = SEARCH.main(data) + tocsp = time.perf_counter() + logger.info(f'Completed parallel SEARCH run on bbo_2d_rosenbrock in {tocsp - ticsp:.4f} seconds.\n') - if (outP[0]["fmin"][0] > 0.0006): - raise ValueError(f"\nParallel Exec: POLL: fmin: {OPS} > {0.0006} \nMADS: fmin = {OMS}\nSearch: fmin = {OSS}") + OMP = out_mads[0]["fmin"][0] + OPP = out_poll[0]["fmin"][0] + OSP = out_search[0]["fmin"][0] - if (outS[0]["fmin"][0] > 0.0006): - raise ValueError(f"\nParallel Exec: Search: fmin {OSS} > {0.0006} \nMADS: fmin = {OMS}\nPoll: fmin = {OPS}") + + toc = time.perf_counter() + logger.info(f'Completed bbo_2d_rosenbrock serial test in {toc - tic:.4f} seconds.') + logger.info(f"\nBest known solution: fmin = {0.}") + logger.info(f"\nSequential Exec: MADS: fmin = {OMS} \nPoll: fmin = {OPS} \nSearch: fmin = {OSS}") + logger.info(f"\nParallel Exec: MADS: fmin: {OMP} \nPoll: fmin = {OPP}\nSearch: fmin = {OSP}") -def test_MADS_callable_quick_const_2d(): + if (OMS > 0.0006): + logger.error(f"Sequential Exec: MADS: fmin: {OMS} > {0.0006}") + raise ValueError(f"\nSequential Exec: MADS: fmin: {OMS} > {0.0006}") + + if (OPS > 0.008): + logger.error(f"Sequential Exec: POLL: fmin: {OPS} > {0.008}") + raise ValueError(f"\nSequential Exec: POLL: fmin: {OPS} > {0.008}") + + if (OSS > 0.0006): + logger.error(f"Sequential Exec: Search: fmin {OSS} > {0.0006}") + raise ValueError(f"\nSequential Exec: Search: fmin {OSS} > {0.0006}") + + if (OMP > 0.05): + logger.error(f"Parallel Exec: MADS: fmin: {OMP} > {0.05}") + raise ValueError(f"\nParallel Exec: MADS: fmin: {OMP} > {0.05}") + + if (OPP > 0.008): + logger.error(f"Parallel Exec: POLL: fmin: {OPP} > {0.008}") + raise ValueError(f"\nParallel Exec: POLL: fmin: {OPP} > {0.008}") + + if (OSP > 0.001): + logger.error(f"Parallel Exec: Search: fmin {OSP} > {0.001}") + raise ValueError(f"\nParallel Exec: Search: fmin {OSP} > {0.001}") + +def test_callable_2d_sin_const(): + logger.info('\nStarted running bbo_2d_sin_const test...') + tic = time.perf_counter() d = 2 - eval = {"blackbox": thin_con} + eval_callable = {"blackbox": thin_con} param = {"name": "thin_con","baseline": [0, -10], "lb": [0, -10], "ub": [25, 10], @@ -109,29 +204,31 @@ def test_MADS_callable_quick_const_2d(): "ns": 100, "visualize": False } - data = {"evaluator": eval, "param": param, "options": options, "sampling": sampling, "search": search} + data = {"evaluator": eval_callable, "param": param, "options": options, "sampling": sampling, "search": search} - outM: Dict = MADS.main(data) - OM = outM[0]["fmin"][0] + out_mads: Dict = MADS.main(data) + OMS = out_mads[0]["fmin"][0] + + toc = time.perf_counter() + logger.info(f'Completed bbo_2d_sin_const run in {toc - tic:.4f} seconds.\n') + logger.info(f"\nBest known solution: fmin = {0.0989}") + logger.info(f"\nSequential Exec: MADS: fmin = {OMS}") - if (outM[0]["fmin"][0] > 0.0989): - raise ValueError(f"MADS: fmin: {OM} > {0.0989}") + if (out_mads[0]["fmin"][0] > 0.0989): + logger.error(f"Sequential Exec: MADS: fmin: {OMS} > {0.0989}") + raise ValueError(f"\nSequential Exec: MADS: fmin: {OMS} > {0.0989}") -def test_MADS_callable_quick_10d(): +def test_callable_quick_10d(): + logger.info('\nStarted running bbo_10d_rosenbrock test...') + tic = time.perf_counter() d = 10 - eval = {"blackbox": rosen} + eval_callable = {"blackbox": rosen} param = {"name": "RB","baseline": [-2.5]*d, "lb": [-5]*d, "ub": [10]*d, "var_names": [f"x{i}" for i in range(d)], "scaling": [15.0]*d, "post_dir": "./post"} - sampling = { - "method": 'ACTIVE', - "ns": int((d+1)*(d+2)/2)+50, - "visualize": False, - "criterion": None - } options = {"seed": 10000, "budget": 10000, "tol": 1e-9, "display": False, "check_cache": True, "store_cache": True, "rich_direction": True, "opportunistic": False, "save_results": False, "isVerbose": False} search = { @@ -146,34 +243,57 @@ def test_MADS_callable_quick_10d(): "visualize": False, "criterion": None } - data = {"evaluator": eval, "param": param, "options": options, "sampling": sampling, "search": search} - outS: Dict = SEARCH.main(data) - OS = outS[0]["fmin"][0] - if (OS > 0.0006): - raise ValueError(f"Search: fmin = {OS} > {0.0006}") + data = {"evaluator": eval_callable, "param": param, "options": options, "sampling": sampling, "search": search} + logger.info('\nStarted running MADS on bbo_10d_rosenbrock serial exectution ...') + ticms = time.perf_counter() + out_mads: Dict = MADS.main(data) + tocms = time.perf_counter() + logger.info(f'Completed serial MADS run on bbo_10d_rosenbrock in {tocms - ticms:.4f} seconds.\n') - outP: Dict = POLL.main(data) - OP = outP[0]["fmin"][0] + ticps = time.perf_counter() + logger.info('\nStarted running POLL on bbo_10d_rosenbrock serial exectution ...') + out_poll: Dict = POLL.main(data) + tocps = time.perf_counter() + logger.info(f'Completed serial POL run on bbo_10d_rosenbrock in {tocps - ticps:.4f} seconds.\n') + + ticss = time.perf_counter() + logger.info('\nStarted running SEARCH on bbo_10d_rosenbrock serial exectution ...') + out_search: Dict = SEARCH.main(data) + tocss = time.perf_counter() + logger.info(f'Completed serial SEARCH run on bbo_10d_rosenbrock in {tocss - ticss:.4f} seconds.\n') + + OSS = out_search[0]["fmin"][0] + OPS = out_poll[0]["fmin"][0] + OMS = out_mads[0]["fmin"][0] - if (OP > 0.25): - raise ValueError(f"POLL: fmin = {OP} > {0.25}") + toc = time.perf_counter() + logger.info(f'Completed bbo_10d_rosenbrock run in {toc - tic:.4f} seconds.') + logger.info(f"\nBest known solution: fmin = {0.}") + logger.info(f"\nSequential Exec: MADS: fmin = {OMS} \nPoll: fmin = {OPS} \nSearch: fmin = {OSS}") + + if (out_mads[0]["fmin"][0] > 0.0006): + logger.error(f"Sequential Exec: MADS: fmin: {OMS} > {0.0006}") + raise ValueError(f"\nSequential Exec: MADS: fmin: {OMS} > {0.0006}") + if (out_poll[0]["fmin"][0] > 0.25): + logger.error(f"Sequential Exec: POLL: fmin: {OPS} > {0.25}") + raise ValueError(f"\nSequential Exec: POLL: fmin: {OPS} > {0.25}") - outM: Dict = MADS.main(data) - OM = outM[0]["fmin"][0] - if (OM > 0.0006): - raise ValueError(f"MADS: fmin = {OM} > {0.0006}") + if (out_search[0]["fmin"][0] > 0.0006): + logger.error(f"Sequential Exec: Search: fmin {OSS} > {0.0006}") + raise ValueError(f"\nSequential Exec: Search: fmin {OSS} > {0.0006}") -def test_MADS_callable_quick_20d(): +def test_callable_quick_20d(): + logger.info('\nStarted running bbo_20d_rosenbrock test...') + tic = time.perf_counter() d = 20 - eval = {"blackbox": rosen} + eval_callable = {"blackbox": rosen} param = {"name": "RB","baseline": [-2.5]*d, "lb": [-5]*d, "ub": [10]*d, "var_names": [f"x{i}" for i in range(d)], "scaling": [15.0]*d, "post_dir": "./post"} - isWin = platform.platform().split('-')[0] == 'Windows' sampling = { "method": 'ACTIVE', @@ -189,53 +309,50 @@ def test_MADS_callable_quick_20d(): "visualize": False } - data = {"evaluator": eval, "param": param, "options": options, "sampling": sampling,"search": search} - outS: Dict = SEARCH.main(data) - SR = outS[0]["fmin"][0] - if (SR > 0.0006 and platform.platform().split('-')[0] == 'Windows'): - raise ValueError(f"Search: fmin {SR} > {0.0006}") + data = {"evaluator": eval_callable, "param": param, "options": options, "sampling": sampling,"search": search} + logger.info('\nStarted running MADS on bbo_20d_rosenbrock serial exectution ...') + ticms = time.perf_counter() + out_mads: Dict = MADS.main(data) + tocms = time.perf_counter() + logger.info(f'Completed serial MADS run on bbo_20d_rosenbrock in {tocms - ticms:.4f} seconds.\n') - outP: Dict = POLL.main(data) - PR = outP[0]["fmin"][0] - if (PR > 2.7 and platform.platform().split('-')[0] == 'Windows'): - raise ValueError(f"POLL: fmin {PR} > {2.7}") + ticps = time.perf_counter() + logger.info('\nStarted running POLL on bbo_20d_rosenbrock serial exectution ...') + out_poll: Dict = POLL.main(data) + tocps = time.perf_counter() + logger.info(f'Completed serial POL run on bbo_20d_rosenbrock in {tocps - ticps:.4f} seconds.\n') + + ticss = time.perf_counter() + logger.info('\nStarted running SEARCH on bbo_20d_rosenbrock serial exectution ...') + out_search: Dict = SEARCH.main(data) + tocss = time.perf_counter() + logger.info(f'Completed serial SEARCH run on bbo_20d_rosenbrock in {tocss - ticss:.4f} seconds.\n') + + OSS = out_search[0]["fmin"][0] + OPS = out_poll[0]["fmin"][0] + OMS = out_mads[0]["fmin"][0] + - outM: Dict = MADS.main(data) - MR = outM[0]["fmin"][0] - if (MR > 0.0006 and platform.platform().split('-')[0] == 'Windows'): - raise ValueError(f"MADS: fmin {MR} > {0.0006}") - -def test_omads_callable_quick_parallel(): - eval = {"blackbox": rosen} - param = {"baseline": [-2.0, -2.0], - "lb": [-5, -5], - "ub": [10, 10], - "var_names": ["x1", "x2"], - "scaling": 10.0, - "post_dir": "./post"} - options = {"seed": 0, "budget": 100, "tol": 1e-6, "display": True, "parallel_mode": True, "save_results": True, "isVerbose": True} - search = { - "type": "sampling", - "s_method": "ACTIVE", - "ns": 10, - "visualize": False - } - data = {"evaluator": eval, "param": param, "options": options, "search": search} + toc = time.perf_counter() + logger.info(f'Completed bbo_20d_rosenbrock run in {toc - tic:.4f} seconds.') + logger.info(f"\nBest known solution: fmin = {0.}") + logger.info(f"\nSequential Exec: MADS: fmin = {OMS} \nPoll: fmin = {OPS} \nSearch: fmin = {OSS}") - out: Dict = MADS.main(data) - print(out) + if (out_mads[0]["fmin"][0] > 0.0006): + logger.error(f"Sequential Exec: MADS: fmin: {OMS} > {0.0006}") + raise ValueError(f"\nSequential Exec: MADS: fmin: {OMS} > {0.0006}") + + if (out_poll[0]["fmin"][0] > 2.7): + logger.error(f"Sequential Exec: POLL: fmin: {OPS} > {2.7}") + raise ValueError(f"\nSequential Exec: POLL: fmin: {OPS} > {2.7}") + + if (out_search[0]["fmin"][0] > 0.0006): + logger.error(f"Sequential Exec: Search: fmin {OSS} > {0.0006}") + raise ValueError(f"\nSequential Exec: Search: fmin {OSS} > {0.0006}") def test_omads_toy_quick(): - assert POLL.DType - assert POLL.Options - assert POLL.Parameters - assert POLL.Evaluator assert POLL.CandidatePoint - assert POLL.Cache - assert POLL.Dirs2n assert POLL.PrePoll - assert POLL.Output - assert POLL.PostMADS assert POLL.main if importlib.util.find_spec('BMDFO'): @@ -243,6 +360,7 @@ def test_omads_toy_quick(): p_file = os.path.abspath("./tests/bm/unconstrained/rosenbrock.json") p_file_2 = os.path.abspath("./tests/bm/constrained/geom_prog.json") else: + is_win = platform.platform().split('-')[0] == 'Windows' p_file = { "evaluator": { @@ -301,15 +419,17 @@ def test_omads_toy_quick(): "LAMBDA": [1E5, 1E5, 1E5, 1E5, 1E5, 1E5], "RHO": 1.0, "post_dir": "./tests/bm/constrained/post", - "h_max": 0.0 - }, + "h_max": 0.0, + "lhs_search_initialization": True + }, + "options": { "seed": 10000, "budget": 100000, "tol": 1e-12, - "psize_init": 2.0, + "psize_init": 2.0 if is_win else 1.0, "display": False, "opportunistic": False, "check_cache": True, @@ -330,61 +450,36 @@ def test_omads_toy_quick(): } } - POLL.main(p_file) - SEARCH.main(p_file) - MADS.main(p_file) + logger.info('\nStarted running bbo_2d_rosenbrock_VNS test...') + tic = time.perf_counter() + out_search: Dict = SEARCH.main(p_file) + OSS = out_search[0]["fmin"][0] + out_poll: Dict = POLL.main(p_file) + OPS = out_poll[0]["fmin"][0] + out_mads: Dict = MADS.main(p_file) + OMS = out_mads[0]["fmin"][0] - outP = POLL.main(p_file_2) - res = outP[0]["fmin"][0] - if (outP[0]["fmin"][0] > 23.8 and platform.platform().split('-')[0] == 'Windows'): - raise ValueError(f"GP: Poll: fmin = {res} > {23.8}") - + toc = time.perf_counter() + logger.info(f'Completed bbo_2d_rosenbrock_VNS run in {toc - tic:.4f} seconds.') + logger.info(f"\nBest known solution: fmin = {0.}") + logger.info(f"\nSequential Exec: MADS: fmin = {OMS} \nPoll: fmin = {OPS} \nSearch: fmin = {OSS}") - data = { - "evaluator": - { - "blackbox": rosen - }, - - "param": - { - "baseline": [-2.0, -2.0], - "lb": [-5, -5], - "ub": [10, 10], - "var_names": ["x1", "x2"], - "scaling": 10.0, - "post_dir": "./tests/bm/unconstrained/post" - }, - - "options": - { - "seed": 0, - "budget": 1000, - "tol": 1e-12, - "psize_init": 1, - "display": False, - "opportunistic": False, - "check_cache": True, - "store_cache": True, - "collect_y": False, - "rich_direction": True, - "precision": "high", - "save_results": False, - "save_coordinates": False, - "save_all_best": False, - "parallel_mode": False - }, + logger.info('\nStarted running bbo_GP_POLL test...') + tic = time.perf_counter() + out_poll = POLL.main(p_file_2) + res = out_poll[0]["fmin"][0] + + toc = time.perf_counter() + logger.info(f'Completed bbo_GP_POLL run in {toc - tic:.4f} seconds.') + logger.info(f"\nBest known solution: {15} < fmin <= {25}") + logger.info(f"\nSequential Exec: Poll: fmin = {res}") - "search": { - "type": "VNS", - "s_method": "LH", - "ns": 10, - "visualize": False - } - } + if (res > 23.8 ): + logger.error(f"\nSequential Exec: POLL: fmin: {res} > {23.8 }") + raise ValueError(f"\nSequential Exec: POLL: fmin: {res} > {23.8 }") - MADS.main(data) + if __name__ == "__main__": freeze_support() diff --git a/tests/test_OMADS_MO_BASIC.py b/tests/test_OMADS_MO_BASIC.py new file mode 100644 index 0000000..c8b55b2 --- /dev/null +++ b/tests/test_OMADS_MO_BASIC.py @@ -0,0 +1,625 @@ +import time +from OMADS import POLL, SEARCH, MADS +import copy +import os +import numpy as np +from typing import Dict, List +from multiprocessing import freeze_support +import platform +import logging + +# Configure the logging +# Create a custom logger +logger = logging.getLogger('OMADS_MO_BBO_unit_test') +logger.setLevel(logging.DEBUG) # Set to DEBUG to capture all messages + +# Create a console handler +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.INFO) # Only log INFO and above to console + +# Create a file handler +file_handler = logging.FileHandler(filename='tests/OMADS_BBO_unit_test.log', mode = 'a') +file_handler.setLevel(logging.DEBUG) # Log all messages to file + +# Create a formatter and set it for handlers +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +console_handler.setFormatter(formatter) +file_handler.setFormatter(formatter) + +# Add handlers to the logger +logger.addHandler(console_handler) +logger.addHandler(file_handler) + +# Example filter to exclude messages from the root logger +class NoRootMessagesFilter(logging.Filter): + def filter(self, record): + return record.name != 'root' + +# Add the filter to handlers +console_handler.addFilter(NoRootMessagesFilter()) +file_handler.addFilter(NoRootMessagesFilter()) + +# logging.basicConfig(level=logging.DEBUG, +# format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', filename='tests/unit_tests_moo.log', filemode='w') + + + +def common_dict(): + outDict: dict = { + "evaluator": + { + "blackbox": None}, + + "param": + { + "baseline": None, + "lb": None, + "ub": None, + "var_names": ["x", "y"], + "fun_names": ["f1", "f2"], + # "constraints_type": ["PB", "PB"], + "nobj": 2, + "isPareto": True, + "scaling": None, + "LAMBDA": [1E5, 1E5], + "RHO": 1.0, + "h_max": np.inf, + "meshType": "GMESH", + "post_dir": None + }, + + "options": + { + "seed": 0, + "budget": 2000, + "tol": 1e-12, + "psize_init": 1, + "display": False, + "opportunistic": False, + "check_cache": True, + "store_cache": True, + "collect_y": False, + "rich_direction": True, + "precision": "medium", + "save_results": True, + "save_coordinates": False, + "save_all_best": False, + "parallel_mode": False + }, + + "search": { + "type": "sampling", + "s_method": "ACTIVE", + "ns": 10, + "visualize": False + }, + } + return outDict + +def MO_Binh_and_Korn(x): + f1 = 4 * x[0]**2 + 4 * x[1]**2 + f2 = (x[0] - 5)**2 + (x[1] - 5)**2 + g1 = (x[0]-5)**2 + x[1]**2 -25 + g2 = 7.7 - (x[0]-8)**2 - (x[1]+3)**2 + + return [[f1, f2], [g1, g2]] + +def MO_Chankong_and_Haimes(x): + f1 = 2 + (x[0]-2)**2 + (x[1]-1)**2 + f2 = 9*x[0]-(x[1]-1)**2 + g1 = x[0]**2 + x[1]**2-225 + g2 = x[0] -3*x[1]+10 + + return [[f1, f2], [g1, g2]] + +def MO_Test_function_4(x): + f1 = x[0]**2-x[1] + f2 = -0.5*x[0]-x[1]-1 + g1 = -(6.5 - (x[0]/6) - x[1]) + g2 = -(7.5 - 0.5 *x[0] -x[1]) + g3 = -(30 - 5*x[0] -x[1]) + + return [[f1, f2], [g1, g2, g3]] + +def MO_Kursawe(x): + f1 = sum([-10*np.exp(-0.2*np.sqrt(x[i]**2 + x[i+1]**2)) for i in range(2)]) + f2 = sum([abs(x[i])**0.8 + 5*np.sin(x[i]**3) for i in range(3)]) + + return [[f1, f2], [0]] + +def MO_Fonseca_Fleming(x): + n = len(x) + f1 = 1 - np.exp(-sum([(x[i]-(1/np.sqrt(n)))**2 for i in range(n)])) + f2 = 1 - np.exp(-sum([(x[i]+(1/np.sqrt(n)))**2 for i in range(n)])) + + return [[f1, f2], [0]] + +def MO_Osyczka_Kundu(x): + f1 = -25*(x[0]-2)**2 - (x[1]-2)**2 - (x[2]-1)**2 - (x[3]-4)**2 - (x[4]-1)**2 + f2 = sum([x[i]**2 for i in range(6)]) + + g1 = x[0] + x[1] -2 + g2 = 6 - x[0] - x[1] + g3 = 2 - x[1] + x[0] + g4 = 2 - x[0] + 3*x[1] + g5 = 4-(x[2]-3)**2 -x[3] + g6 = (x[4]-3)**2 + x[5] -4 + + return [[f1, f2], [-g1, -g2, -g3, -g4, -g5, -g6]] + +def MO_CTP1(x): + f1 = x[0] + f2 = (1+x[1])*np.exp(-(x[0])/(1+x[1])) + g1 = 1-((f2)/(0.858*np.exp(-0.541*f1))) + g2 = 1-(f2/(0.728*np.exp(-0.295*f1))) + + return [[f1, f2], [g1, g2]] + +def MO_Ex(x): + f1 = x[0] + f2 = (1+x[1])/x[0] + + g1 = 6-(x[1]+9*x[0]) + g2 = 1+x[1] - 9*x[0] + + return [[f1,f2],[g1,g2]] + +def MO_ZDT1(x): + f1 = x[0] # objective 1 + g = 1 + 9 * np.sum(np.divide(x[1:len(x)], (len(x) - 1))) + h = 1 - np.sqrt(f1 / g) + f2 = g * h # objective 2 + + return [[f1, f2], [0]] + +def MO_ZDT3(x): + f1 = x[0] # objective 1 + g = 1 + (9/(len(x) - 1)) * np.sum(x[1:len(x)]) + h = 1 - np.sqrt(f1 / g) - (f1/g)*np.sin(10*np.pi*f1) + f2 = g * h # objective 2 + + return [[f1, f2], [0]] + +def MO_ZDT4(x): + f1 = x[0] # objective 1 + g = 1 + 10*(len(x)-1) + np.sum([x[i]**2 - 10*np.cos(4*np.pi*x[i]) for i in range(1, len(x))]) + h = 1 - np.sqrt(f1 / g) + f2 = g * h # objective 2 + + return [[f1, f2], [0]] + +def MO_ZDT6(x): + f1 = 1 - np.exp(-4*x[0]) * np.sin(6*np.pi*x[0])**6 + g = 1+9*(sum(x[1:len(x)])/9)**.25 + h = 1 - (f1/g)**2 + f2 = g * h # objective 2 + + return [[f1, f2], [0]] + +def test_MO_Binh_and_Korn(): + logger.info('\nStarted running MO_Binh_and_Korn test... \n') + tic = time.perf_counter() + data = common_dict() + data["evaluator"]["blackbox"] = MO_Binh_and_Korn + data["param"]["name"] = "Binh_and_Korn" + data["param"]["baseline"] = [0, 0] + data["param"]["lb"] = [0, 0] + data["param"]["ub"] = [5, 3] + data["param"]["constraints_type"] = ["PB", "PB"] + data["param"]["scaling"] = [5, 3] + data["param"]["post_dir"] = "./tests/bm/MOO/constrained/Binh_and_Korn/post" + data["options"]["budget"] = 500 + data["param"]["ref_point"] = [140, 50] + + p_out, _ = POLL.main(data) + s_out, _ = SEARCH.main(data) + m_out, _ = MADS.main(data) + PHV = p_out["HV"] + SHV = s_out["HV"] + MHV = m_out["HV"] + + toc = time.perf_counter() + logger.info(f'Completed MO_Binh_and_Korn run in {toc - tic:.4f} seconds.\n') + logger.info(f"Hypervolume indicators:\n poll step HV_expected = {0.8}\n poll step HV_obtained = {PHV}\n search step HV_expected = {0.79}\n search step HV_obtained = {SHV}\n MADS HV_expected = {0.8}\n MADS HV_obtained = {MHV}\n") + if PHV < 0.8 or SHV < 0.79 or MHV < 0.8: + logger.error("The MO_Binh_and_Korn QA test failed: \n hypervolume indicators obtained does not pass the success criteria \n") + raise IOError("The MO_Binh_and_Korn QA test completed but failed.") + else: + logger.info("The MO_Binh_and_Korn QA test successfully passed: \n hypervolume indicators obtained pass the success criteria \n") + +def test_MO_Chankong_and_Haimes(): + logger.info('\nStarted running MO_Chankong_and_Haimes test... \n') + tic = time.perf_counter() + data = common_dict() + data["evaluator"]["blackbox"] = MO_Chankong_and_Haimes + data["param"]["name"] = "Chankong_and_Haimes" + data["param"]["baseline"] = [0, 0] + data["param"]["lb"] = [-20, -20] + data["param"]["ub"] = [20, 20] + data["param"]["constraints_type"] = ["PB", "PB"] + data["param"]["scaling"] = [40, 40] + data["param"]["post_dir"] = "./tests/bm/MOO/constrained/Chankong_and_Haimes/post" + data["options"]["budget"] = 500 + data["param"]["ref_point"] = [275, 0.1] + + # POLL.main(data) + # SEARCH.main(data) + p_out, _ = POLL.main(data) + s_out, _ = SEARCH.main(data) + m_out, _ = MADS.main(data) + PHV = p_out["HV"] + SHV = s_out["HV"] + MHV = m_out["HV"] + + toc = time.perf_counter() + logger.info(f'Completed MO_Chankong_and_Haimes run in {toc - tic:.4f} seconds.\n') + logger.info(f"Hypervolume indicators:\n poll step HV_expected = {0.8}\n poll step HV_obtained = {PHV}\n search step HV_expected = {0.6}\n search step HV_obtained = {SHV}\n MADS HV_expected = {0.8}\n MADS HV_obtained = {MHV}\n") + if PHV < 0.8 or SHV < 0.6 or MHV < 0.8: + logger.error("The MO_Chankong_and_Haimes QA test failed: \n hypervolume indicators obtained does not pass the success criteria \n") + raise IOError("The MO_Chankong_and_Haimes QA test completed but failed.") + else: + logger.info("The MO_Chankong_and_Haimes QA test successfully passed: \n hypervolume indicators obtained pass the success criteria \n") + +def test_MO_Fonseca_Fleming(): + logger.info('\nStarted running MO_Fonseca_Fleming test... \n') + tic = time.perf_counter() + data = common_dict() + data["evaluator"]["blackbox"] = MO_Fonseca_Fleming + data["param"]["name"] = "Fonseca_Fleming" + data["param"]["baseline"] = [0, 0] + data["param"]["lb"] = [-4, -4] + data["param"]["ub"] = [4, 4] + data["meshType"] = "GMESH" + # data["param"]["constraints_type"] = ["EB"] + data["param"]["scaling"] = [8, 8] + data["param"]["post_dir"] = "./tests/bm/MOO/unconstrained/Fonseca_Fleming/post" + data["options"]["budget"] = 500 + data["param"]["ref_point"] = [1, 1] + + # POLL.main(data) + # SEARCH.main(data) + p_out, _ = POLL.main(data) + s_out, _ = SEARCH.main(data) + m_out, _ = MADS.main(data) + PHV = p_out["HV"] + SHV = s_out["HV"] + MHV = m_out["HV"] + + toc = time.perf_counter() + logger.info(f'Completed MO_Fonseca_Fleming run in {toc - tic:.4f} seconds.\n') + logger.info(f"Hypervolume indicators:\n poll step HV_expected = {0.34}\n poll step HV_obtained = {PHV}\n search step HV_expected = {0.53}\n search step HV_obtained = {SHV}\n MADS HV_expected = {0.35}\n MADS HV_obtained = {MHV}\n") + if PHV < 0.34 or SHV < 0.34 or MHV < 0.35: + logger.error("The MO_Fonseca_Fleming QA test failed: \n hypervolume indicators obtained does not pass the success criteria \n") + raise IOError("The MO_Fonseca_Fleming QA test completed but failed.") + else: + logger.info("The MO_Fonseca_Fleming QA test successfully passed: \n hypervolume indicators obtained pass the success criteria \n") + +def test_MO_Test_function_4(): + logger.info('\nStarted running MO_Test_function test... \n') + tic = time.perf_counter() + data = common_dict() + data["evaluator"]["blackbox"] = MO_Test_function_4 + data["param"]["name"] = "Test_function_4" + data["param"]["baseline"] = [0, 0]#[3, 3] + data["param"]["lb"] = [-7, -7] + data["param"]["ub"] = [4, 4] + data["meshType"] = "GMESH" + data["param"]["constraints_type"] = ["PB", "PB"] + data["param"]["scaling"] = [10, 10] + data["param"]["post_dir"] = "./tests/bm/MOO/constrained/Test_function_4/post" + data["options"]["budget"] = 500 + data["param"]["ref_point"] = [12, -5] + + + + p_out, _ = POLL.main(data) + m_out, _ = MADS.main(data) + s_out, _ = SEARCH.main(data) + + PHV = p_out["HV"] + SHV = s_out["HV"] + MHV = m_out["HV"] + + toc = time.perf_counter() + logger.info(f'Completed MO_Test_function_4 run in {toc - tic:.4f} seconds.\n') + logger.info(f"Hypervolume indicators:\n poll step HV_expected = {0.6}\n poll step HV_obtained = {PHV}\n search step HV_expected = {0.4}\n search step HV_obtained = {SHV}\n MADS HV_expected = {0.6}\n MADS HV_obtained = {MHV}\n") + if PHV < 0.6 or SHV < 0.4 or MHV < 0.6: + logger.error("The MO_Test_function_4 QA test failed: \n hypervolume indicators obtained does not pass the success criteria \n") + raise IOError("The MO_Test_function_4 QA test completed but failed.") + else: + logger.info("The MO_Test_function_4 QA test successfully passed: \n hypervolume indicators obtained pass the success criteria \n") + +def test_MO_Kursawe(): + # TODO: uncon logic needs review + logger.info('\nStarted running MO_Kursawe test... \n') + tic = time.perf_counter() + data = common_dict() + data["evaluator"]["blackbox"] = MO_Kursawe + data["param"]["name"] = "Kursawe" + # data["param"]["baseline"] = [-2.0, 0.5, -4.5] + data["param"]["baseline"] = [-2.0, -0.5, -5] + data["param"]["var_names"] = ['x1', 'x2', 'x3'] + data["param"]["lb"] = [-5, -5, -5] + data["param"]["ub"] = [5, 5, 5] + # data["param"]["LAMBDA"]= None + # data["param"]["RHO"] = 1 + # data["param"]["h_max"] = 0 + data["meshType"] = "GMESH" + # data["param"]["constraints_type"] = ["PB"] + data["param"]["scaling"] = [10, 10, 10] + data["param"]["post_dir"] = "./tests/bm/MOO/unconstrained/Kursawe/post" + data["options"]["budget"] = 1000 + data["param"]["ref_point"] = [-14, 1] + + # POLL.main(data) + # SEARCH.main(data) + p_out, _ = POLL.main(data) + s_out, _ = SEARCH.main(data) + m_out, _ = MADS.main(data) + PHV = p_out["HV"] + SHV = s_out["HV"] + MHV = m_out["HV"] + + toc = time.perf_counter() + logger.info(f'Completed MO_Kursawe run in {toc - tic:.4f} seconds.\n') + logger.info(f"Hypervolume indicators:\n poll step HV_expected = {0.64}\n poll step HV_obtained = {PHV}\n search step HV_expected = {0.5}\n search step HV_obtained = {SHV}\n MADS HV_expected = {0.45}\n MADS HV_obtained = {MHV}\n") + if PHV < 0.64 or SHV < 0.5 or MHV < 0.45: + logger.error("The MO_Kursawe QA test failed: \n hypervolume indicators obtained does not pass the success criteria \n") + raise IOError("The MO_Kursawe QA test completed but failed.") + else: + logger.info("The MO_Kursawe QA test successfully passed: \n hypervolume indicators obtained pass the success criteria \n") + +def test_MO_Osyczka_Kundu(): + # COMPLETED: Investigate why starting from infeasible point does not work in MOO + logger.info('\nStarted running MO_Osyczka_Kundu test... \n') + tic = time.perf_counter() + data = common_dict() + data["evaluator"]["blackbox"] = MO_Osyczka_Kundu + data["param"]["name"] = "Osyczka_Kundu" + data["param"]["baseline"] = [3, 2, 2, 0, 5, 10] + # data["param"]["baseline"] = [5, 1, 5, 0, 5, 8] + data["param"]["var_names"] = ['x1', 'x2', 'x3', 'x4', 'x5', 'x6'] + data["param"]["lb"] = [0, 0, 1, 0, 1, 0] + data["param"]["ub"] = [10, 10, 5, 6, 5, 10] + data["param"]["meshType"] = "GMESH" + data["param"]["constraints_type"] = ["PB"]*6 + data["param"]["scaling"] = [10, 10, 4, 6, 4, 10] + data["param"]["post_dir"] = "./tests/bm/MOO/constrained/Osyczka_Kundu/post" + data["options"]["budget"] = 10000 + data["options"]["seed"] = 1234 + data["param"]["ref_point"] = [-50, 80] + is_win = platform.platform().split('-')[0] == 'Windows' + data["param"]["lhs_search_initialization"] = False if is_win else True + + data["search"]["ns"] = 50 + p_out, _ = POLL.main(data) + s_out, _ = SEARCH.main(data) + data["search"]["ns"] = 150 + m_out, _ = MADS.main(data) + PHV = p_out["HV"] + SHV = s_out["HV"] + MHV = m_out["HV"] + toc = time.perf_counter() + logger.info(f'Completed MO_Osyczka_Kundu run in {toc - tic:.4f} seconds.\n') + logger.info(f"Hypervolume indicators:\n poll step HV_expected = {1.3}\n poll step HV_obtained = {PHV}\n search step HV_expected = {1.0}\n search step HV_obtained = {SHV}\n MADS HV_expected = {1.8}\n MADS HV_obtained = {MHV}\n") + if PHV < 1.3 or SHV < 1.0 or MHV < 1.15: + logger.error("The MO_Osyczka_Kundu QA test failed: \n hypervolume indicators obtained does not pass the success criteria \n") + raise IOError("The MO_Osyczka_Kundu QA test completed but failed.") + else: + logger.info("The MO_Osyczka_Kundu QA test successfully passed: \n hypervolume indicators obtained pass the success criteria \n") + +def test_MO_CTP1(): + logger.info('\nStarted running MO_CTP1 test... \n') + tic = time.perf_counter() + data = common_dict() + data["evaluator"]["blackbox"] = MO_CTP1 + data["param"]["name"] = "MO_CTP1" + data["param"]["baseline"] = [0.5, 0.5] + data["param"]["var_names"] = ['x1', 'x2'] + data["param"]["lb"] = [0, 0] + data["param"]["ub"] = [1, 1] + data["param"]["meshType"] = "GMESH" + data["param"]["constraints_type"] = ["PB"]*2 + data["param"]["scaling"] = [1, 1] + data["param"]["post_dir"] = "./tests/bm/MOO/constrained/MO_CTP1/post" + data["options"]["budget"] = 500 + data["search"]["ns"] = 50 + data["param"]["ref_point"] = [1, 1] + p_out, _ = POLL.main(data) + s_out, _ = SEARCH.main(data) + m_out, _ = MADS.main(data) + PHV = p_out["HV"] + SHV = s_out["HV"] + MHV = m_out["HV"] + + toc = time.perf_counter() + logger.info(f'Completed MO_CTP1 run in {toc - tic:.4f} seconds.\n') + logger.info(f"Hypervolume indicators:\n poll step HV_expected = {0.6}\n poll step HV_obtained = {PHV}\n search step HV_expected = {0.6}\n search step HV_obtained = {SHV}\n MADS HV_expected = {0.65}\n MADS HV_obtained = {MHV}\n") + if PHV < 0.6 or SHV < 0.6 or MHV < 0.65: + logger.error("The MO_CTP1 QA test failed: \n hypervolume indicators obtained does not pass the success criteria \n") + raise IOError("The MO_CTP1 QA test completed but failed.") + else: + logger.info("The MO_CTP1 QA test successfully passed: \n hypervolume indicators obtained pass the success criteria \n") + +def test_MO_Ex(): + logger.info('\nStarted running MO_Ex test... \n') + tic = time.perf_counter() + data = common_dict() + data["evaluator"]["blackbox"] = MO_Ex + data["param"]["name"] = "Ex" + data["param"]["baseline"] = [0.6, 2.5] + data["param"]["var_names"] = ['x1', 'x2'] + data["param"]["lb"] = [0.1, 0] + data["param"]["ub"] = [1, 5] + data["param"]["meshType"] = "GMESH" + data["param"]["constraints_type"] = ["PB"]*2 + data["param"]["scaling"] = [0.9, 5] + data["param"]["post_dir"] = "./tests/bm/MOO/constrained/Ex/post" + data["options"]["budget"] = 1000 + data["search"]["ns"] = 15 + data["param"]["ref_point"] = [1, 9] + + p_out, _ = POLL.main(data) + s_out, _ = SEARCH.main(data) + m_out, _ = MADS.main(data) + PHV = p_out["HV"] + SHV = s_out["HV"] + MHV = m_out["HV"] + + toc = time.perf_counter() + logger.info(f'Completed MO_Ex run in {toc - tic:.4f} seconds.\n') + logger.info(f"Hypervolume indicators:\n poll step HV_expected = {1.2}\n poll step HV_obtained = {PHV}\n search step HV_expected = {0.8}\n search step HV_obtained = {SHV}\n MADS HV_expected = {0.8}\n MADS HV_obtained = {MHV}\n") + if PHV < 1.2 or SHV < 0.8 or MHV < 0.8: + logger.error("The MO_Ex QA test failed: \n hypervolume indicators obtained does not pass the success criteria \n") + raise IOError("The MO_Ex QA test completed but failed.") + else: + logger.info("The MO_Ex QA test successfully passed: \n hypervolume indicators obtained pass the success criteria \n") + +def test_MO_ZDT1(): + logger.info('\nStarted running MO_ZDT1 test... \n') + tic = time.perf_counter() + d = 30 + data = common_dict() + data["evaluator"]["blackbox"] = MO_ZDT1 + data["param"]["name"] = "MO_ZDT1" + np.random.seed(seed= 12345) + data["param"]["baseline"] = np.random.rand(d) + data["param"]["var_names"] = [f'x{i}' for i in range(d)] + data["param"]["lb"] = [0]*d + data["param"]["ub"] = [1]*d + data["param"]["meshType"] = "GMESH" + data["param"]["constraints_type"] = ["PB"] + data["param"]["scaling"] = [1]*d + data["param"]["post_dir"] = "./tests/bm/MOO/unconstrained/MO_ZDT1/post" + data["options"]["budget"] = 10000 + data["param"]["ref_point"] = [1, 1] + + p_out, _ = POLL.main(data) + data["options"]["budget"] = 500 + s_out, _ = SEARCH.main(data) + data["options"]["budget"] = 10000 + m_out, _ = MADS.main(data) + PHV = p_out["HV"] + SHV = s_out["HV"] + MHV = m_out["HV"] + + toc = time.perf_counter() + logger.info(f'Completed MO_ZDT1 run in {toc - tic:.4f} seconds.\n') + logger.info(f"Hypervolume indicators:\n poll step HV_expected = {0.62}\n poll step HV_obtained = {PHV}\n search step HV_expected = {0.7}\n search step HV_obtained = {SHV}\n MADS HV_expected = {0.62}\n MADS HV_obtained = {MHV}\n") + if PHV < 0.62 or SHV < 0.7 or MHV < 0.62: + logger.error("The MO_ZDT1 QA test failed: \n hypervolume indicators obtained does not pass the success criteria \n") + raise IOError("The MO_ZDT1 QA test completed but failed.") + else: + logger.info("The MO_ZDT1 QA test successfully passed: \n hypervolume indicators obtained pass the success criteria \n") + +def test_MO_ZDT3(): + logger.info('\nStarted running MO_ZDT3 test... \n') + tic = time.perf_counter() + d = 30 + data = common_dict() + data["evaluator"]["blackbox"] = MO_ZDT3 + data["param"]["name"] = "MO_ZDT3" + np.random.seed(seed= 12345) + data["param"]["baseline"] = np.random.rand(d) + data["param"]["var_names"] = [f'x{i}' for i in range(d)] + data["param"]["lb"] = [0]*d + data["param"]["ub"] = [1]*d + data["param"]["meshType"] = "GMESH" + data["param"]["constraints_type"] = ["PB"] + data["param"]["scaling"] = [1]*d + data["param"]["post_dir"] = "./tests/bm/MOO/unconstrained/MO_ZDT3/post" + data["options"]["budget"] = 10000 + data["search"]["ns"] = 50 + data["param"]["ref_point"] = [1, 1] + + p_out, _ = POLL.main(data) + s_out, _ = SEARCH.main(data) + m_out, _ = MADS.main(data) + PHV = p_out["HV"] + SHV = s_out["HV"] + MHV = m_out["HV"] + + toc = time.perf_counter() + logger.info(f'Completed MO_ZDT3 run in {toc - tic:.4f} seconds.\n') + logger.info(f"Hypervolume indicators:\n poll step HV_expected = {0.6}\n poll step HV_obtained = {PHV}\n search step HV_expected = {0.7}\n search step HV_obtained = {SHV}\n MADS HV_expected = {0.6}\n MADS HV_obtained = {MHV}\n") + if PHV < 0.6 or SHV < 0.7 or MHV < 0.6: + logger.error("The MO_ZDT3 QA test failed: \n hypervolume indicators obtained does not pass the success criteria \n") + raise IOError("The MO_ZDT3 QA test completed but failed.") + else: + logger.info("The MO_ZDT3 QA test successfully passed: \n hypervolume indicators obtained pass the success criteria \n") + +def test_MO_ZDT4(): + logger.info('\nStarted running MO_ZDT4 test... \n') + tic = time.perf_counter() + d = 10 + data = common_dict() + data["evaluator"]["blackbox"] = MO_ZDT4 + data["param"]["name"] = "MO_ZDT4" + np.random.seed(seed= 12345) + data["param"]["baseline"] = np.random.rand(1).tolist() + np.random.uniform(low=-10, high=10, size=(d-1,)).tolist() + data["param"]["var_names"] = [f'x{i}' for i in range(d)] + data["param"]["lb"] = [0] + [-10]*(d-1) + data["param"]["ub"] = [1] + [10]*(d-1) + data["param"]["meshType"] = "GMESH" + data["param"]["constraints_type"] = ["PB"] + data["param"]["scaling"] = [1] + [20]*(d-1) + data["param"]["post_dir"] = "./tests/bm/MOO/unconstrained/MO_ZDT4/post" + data["options"]["budget"] = 2000 #40000 + data["search"]["ns"] = 55 + data["param"]["ref_point"] = [1, 1.2] + data["param"]["lhs_search_initialization"] = True + + m_out, _ = MADS.main(data) + MHV = m_out["HV"] + + toc = time.perf_counter() + logger.info(f'Completed MO_ZDT4 run in {toc - tic:.4f} seconds.\n') + logger.info(f"Hypervolume indicators:\n MADS step HV_expected = {0.8}\n MADS HV_obtained = {MHV}\n") + if MHV < 0.8: + logger.error("The MO_ZDT4 QA test failed: \n hypervolume indicators obtained does not pass the success criteria \n") + raise IOError("The MO_ZDT4 QA test completed but failed.") + else: + logger.info("The MO_ZDT4 QA test successfully passed: \n hypervolume indicators obtained pass the success criteria \n") + +def test_MO_ZDT6(): + logger.info('\nStarted running MO_ZDT6 test... \n') + tic = time.perf_counter() + d = 10 + data = common_dict() + data["evaluator"]["blackbox"] = MO_ZDT6 + data["param"]["name"] = "MO_ZDT6" + np.random.seed(seed= 12345) + data["param"]["baseline"] = np.random.rand(d) + data["param"]["var_names"] = [f'x{i}' for i in range(d)] + data["param"]["lb"] = [0]*d + data["param"]["ub"] = [1]*d + + data["param"]["constraints_type"] = ["PB"] + data["param"]["scaling"] = [1]*d + data["param"]["post_dir"] = "./tests/bm/MOO/unconstrained/MO_ZDT6/post" + + data["search"]["ns"] = 100 + data["param"]["ref_point"] = [1, 1.2] + data["options"]["budget"] = 10000 #10000 + data["param"]["meshType"] = "GMESH" + p_out, _ = POLL.main(data) + data["options"]["budget"] = 500 #10000 + data["param"]["meshType"] = "OMESH" + s_out, _ = SEARCH.main(data) + m_out, _ = MADS.main(data) + PHV = p_out["HV"] + SHV = s_out["HV"] + MHV = m_out["HV"] + + toc = time.perf_counter() + logger.info(f'Completed MO_ZDT6 run in {toc - tic:.4f} seconds.\n') + logger.info(f"Hypervolume indicators:\n poll step HV_expected = {0.4}\n poll step HV_obtained = {PHV}\n search step HV_expected = {0.1}\n search step HV_obtained = {SHV}\n MADS HV_expected = {0.9}\n MADS HV_obtained = {MHV}\n") + if PHV < 0.4 or SHV < 0.1 or MHV < 0.9: + logger.error("The MO_ZDT6 QA test failed: \n hypervolume indicators obtained does not pass the success criteria \n") + raise IOError("The MO_ZDT6 QA test completed but failed.") + else: + logger.info("The MO_ZDT6 QA test successfully passed: \n hypervolume indicators obtained pass the success criteria \n") + + +if __name__ == "__main__": + freeze_support()