PageRenderTime 57ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/pyGDM2/EO/problems.py

https://gitlab.com/wiechapeter/pyGDM2-dirty
Python | 487 lines | 437 code | 9 blank | 41 comment | 0 complexity | bbc4e8d6b7e0ae53acf6e5a24784ba00 MD5 | raw file
  1. # encoding: utf-8
  2. """
  3. Collection of problems for the EO submodule of pyGDM2
  4. Copyright (C) 2017, P. R. Wiecha
  5. This program is free software: you can redistribute it and/or modify
  6. it under the terms of the GNU General Public License as published by
  7. the Free Software Foundation, either version 3 of the License, or
  8. (at your option) any later version.
  9. This program is distributed in the hope that it will be useful,
  10. but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. GNU General Public License for more details.
  13. You should have received a copy of the GNU General Public License
  14. along with this program. If not, see <http://www.gnu.org/licenses/>.
  15. """
  16. import numpy as np
  17. import warnings
  18. from .. import core
  19. from .. import linear
  20. from .. import tools
  21. class BaseProblem(object):
  22. """Base pyGDM2 problem
  23. Problem classes must call the `BaseProblem` constructor with at least
  24. an instance of a model-class (see :class:`.models.BaseModel`)
  25. Parameters
  26. ----------
  27. model : instance of class implementing :class:`.models.BaseModel`
  28. Definition of structure model and pyGDM-simulation
  29. field_index : int, default: 0
  30. field_index to use from pyGDM simulation.
  31. maximize : bool, default: True
  32. whether to maximize (True) or minimize (False) the fitness function
  33. nthreads : int, default: -1
  34. number of threads for multithreading.
  35. Not directly used, stored internally as `self.nthreads`. Using it
  36. lies in the user's responsibility ;-)
  37. """
  38. __name__ = 'BaseProblem'
  39. def __init__(self, model=None, field_index=0, maximize=True, nthreads=-1):
  40. if model is not None:
  41. ## --- main simulation definitions
  42. self.model = model
  43. self.sim = model.sim
  44. self.field_index = field_index
  45. self.maximize = maximize
  46. self.nthreads = nthreads
  47. ## -- problem boundaries
  48. lbnds, ubnds = self.model.get_bounds()
  49. self.set_bounds(lbnds, ubnds)
  50. else:
  51. raise ValueError("No valid model provided. Please init the Problem with a valid geometry model instance.")
  52. #==============================================================================
  53. # Mandatory reinmplementations for specific problems:
  54. #==============================================================================
  55. def objective_function(self, params):
  56. """To be reimplemented! Evaluates the objective function"""
  57. raise NotImplementedError("'problems.BaseProblem.objective_function' not re-implemented!")
  58. #==============================================================================
  59. # Optionally reinmplementations
  60. #==============================================================================
  61. def equality_constraints(self, params):
  62. """Optional (nonlinear) equality constraints (as 1D array)
  63. Equality constraints are regarded satisfied if == 0
  64. """
  65. return []
  66. def inequality_constraints(self, params):
  67. """Optional (nonlinear) inequality constraints (as 1D array)
  68. Inequality constraints are regarded satisfied if <= 0
  69. """
  70. return []
  71. def get_name(self):
  72. """Return problem object name"""
  73. return self.__name__
  74. def get_extra_info(self):
  75. """Return extra info, e.g. a more detailed problem description"""
  76. return ""
  77. def get_nobj(self):
  78. """number of objectives"""
  79. return len(self.fitness(self.lbnds))
  80. #==============================================================================
  81. # Internally used
  82. #==============================================================================
  83. def set_bounds(self, lbnds, ubnds):
  84. self.lbnds = lbnds
  85. self.ubnds = ubnds
  86. def get_bounds(self):
  87. return (self.lbnds, self.ubnds)
  88. def fitness(self, dv):
  89. """Virtual objective function, single objective (PyGMO requirement)"""
  90. ## --- target function evaluation
  91. fitness = self.objective_function(dv)
  92. if not hasattr(fitness, '__iter__'):
  93. fitness = [fitness, ]
  94. if self.maximize:
  95. fitness = [-1*f for f in fitness]
  96. ## --- constraint functions
  97. equality_constraints = self.equality_constraints(dv) # ==0 --> satisfied
  98. if not hasattr(equality_constraints, '__iter__'):
  99. equality_constraints = [equality_constraints, ]
  100. inequality_constraints = self.inequality_constraints(dv) # <=0 --> satisfied
  101. if not hasattr(inequality_constraints, '__iter__'):
  102. inequality_constraints = [inequality_constraints, ]
  103. return_vector = np.concatenate( [ fitness, equality_constraints, inequality_constraints ] )
  104. return return_vector
  105. # =============================================================================
  106. # Pre-defined Problems
  107. # =============================================================================
  108. class ProblemScat(BaseProblem):
  109. """optimize for scat./ext./abs. cross-section or efficiency
  110. Parameters
  111. ----------
  112. model : instance of class implementing :class:`.models.BaseModel`
  113. Definition of structure model and pyGDM-simulation
  114. field_index : int, default: 0
  115. field_index to use from pyGDM simulation.
  116. opt_target : str, default: 'Qscat'
  117. Optimization target.
  118. One of ['Qscat', 'Qext', 'Qabs', 'CSscat', 'CSext', 'CSabs']
  119. - CS: cross-section (absolute value)
  120. - Q: efficiency (CS divided by geometrical CS)
  121. maximize : bool, default: True
  122. whether to maximize (True) or minimize (False) the fitness function
  123. nthreads : int, default: -1
  124. passed to :func:`pyGDM2.core.scatter`
  125. """
  126. def __init__(self, model, field_index=0, opt_target='Qscat',
  127. maximize=True, nthreads=-1):
  128. """constructor"""
  129. super(self.__class__, self).__init__(model, field_index,
  130. maximize=maximize, nthreads=nthreads)
  131. self.opt_target = opt_target
  132. def objective_function(self, params):
  133. """evaluate directionality ratio
  134. """
  135. self.model.generate_structure(params)
  136. ## --- GDM simulation and cross-section calculation
  137. core.scatter(self.model.sim, nthreads=self.nthreads, verbose=0)
  138. ext_cs, sca_cs, abs_cs = linear.extinct(self.model.sim, field_index=self.field_index)
  139. ## --- scattering
  140. if self.opt_target == "Qscat":
  141. geo_sect = tools.get_geometric_cross_section(self.model.sim)
  142. val = float(sca_cs) / geo_sect
  143. elif self.opt_target == "CSscat":
  144. val = float(sca_cs)
  145. ## --- extinction
  146. elif self.opt_target == "Qext":
  147. geo_sect = tools.get_geometric_cross_section(self.model.sim)
  148. val = float(ext_cs) / geo_sect
  149. elif self.opt_target == "CSext":
  150. val = float(ext_cs)
  151. ## --- absorption
  152. elif self.opt_target == "Qabs":
  153. geo_sect = tools.get_geometric_cross_section(self.model.sim)
  154. val = float(abs_cs) / geo_sect
  155. elif self.opt_target == "CSabs":
  156. val = float(abs_cs)
  157. return val
  158. def get_extra_info(self):
  159. return "\n\tMaximization of scattering cross section"
  160. class ProblemNearfield(BaseProblem):
  161. """optimize near-field enhancement ($|E|^2$ or $|B|^2$)
  162. opt_target defines wether the E-field ('E') or the B-field ('B')
  163. shall be maximized (or optionally minimized)
  164. Parameters
  165. ----------
  166. model : instance of class implementing :class:`.models.BaseModel`
  167. Definition of structure model and pyGDM-simulation
  168. field_index : int, default: 0
  169. field_index to use from pyGDM simulation.
  170. r_probe : 3-tuple, default: (0,0,0)
  171. defines the (x,y,z) position where field enhancemnt is to be
  172. maximized / minimized
  173. opt_target : str, default: 'E'
  174. Optimization target. Electric or magnetic field intensity.
  175. One of ['E', 'B'].
  176. maximize : bool, default: True
  177. whether to maximize (True) or minimize (False) the fitness function
  178. nthreads : int, default: -1
  179. passed to :func:`pyGDM2.core.scatter`
  180. """
  181. def __init__(self, model, field_index=0,
  182. r_probe=(0,0,0), opt_target='E',
  183. maximize=True, nthreads=-1):
  184. """constructor"""
  185. super(self.__class__, self).__init__(model, field_index,
  186. maximize=maximize, nthreads=nthreads)
  187. self.r_probe = np.transpose( [r_probe] )
  188. self.opt_target = opt_target.lower()
  189. if self.opt_target not in ['e', 'b', 'h']:
  190. raise ValueError("'opt_target' must be one of ['e', 'b', 'h'].")
  191. def objective_function(self, params):
  192. """evaluate field intensity"""
  193. self.model.generate_structure(params)
  194. ## --- GDM simulation and cross-section calculation
  195. core.scatter(self.model.sim, nthreads=self.nthreads, verbose=0)
  196. Es, Etot, Bs, Btot = linear.nearfield(self.model.sim,
  197. field_index=self.field_index,
  198. r_probe=self.r_probe)
  199. if self.opt_target.lower() == "e":
  200. a = Es
  201. elif self.opt_target.lower() in ["b", "h"]:
  202. a = Bs
  203. I_NF = np.abs(a[0][3]**2 + a[0][4]**2 + a[0][5]**2)
  204. return I_NF
  205. def get_extra_info(self):
  206. return "\n\tMaximization of near-field intensity"
  207. class ProblemDirectivity(BaseProblem):
  208. """Problem to optimize directionality of scattering from nanostructure
  209. Use `EO.tools.calculate_solid_angle_by_dir_index` to define 'dir_index'
  210. for the desired target solid angle
  211. Parameters
  212. ----------
  213. model : instance of class implementing :class:`.models.BaseModel`
  214. Definition of structure model and pyGDM-simulation
  215. field_index : list of int, default: 0
  216. "field_index" in case of multiple pyGDM-simulation configurations
  217. dir_index : list of int; list of list of int, default: [5]
  218. which solid-angle elements to consider for optimization.
  219. If list of lists of int (e.g. [[4], [5], [1,2]]), will run
  220. multi-objective optimization using each of the sub-lists as target
  221. solid angle
  222. which_field : str, default: 'e_tot'
  223. optimize using one of ['e_sc', 'e_tot', 'e0'].
  224. - 'e_sc': scattered field
  225. - 'e_tot': total field (= e0 + e_sc)
  226. - 'e0': fundamental field (rather for testing)
  227. kwargs_farfield : dict, default: dict(Nteta=3, Nphi=5, tetamin=0, tetamax=np.pi/2.)
  228. kwargs, passed to :func:`pyGDM2.linear.farfield`, defining the farfield
  229. scattering calculation. Required arguments are: Nteta, Nphi, tetamin, tetamax
  230. consider_dS (False), averaging (True), absoluteI (False)
  231. Additional flags (defaults in paranthesis):
  232. - consider_dS: correct (True) solid angle integration or simple sum (False)
  233. - averaging: avgerage (True) or integrate (False) solid angle intensity
  234. - absoluteI: maximize total intensity through selected solid angle instead of ratio
  235. kwargs_scatter : dict, default: {}
  236. additional kwargs, passed to :func:`pyGDM2.core.sactter`
  237. maximize : bool, default: True
  238. whether to maximize (True) or minimize (False) the fitness function
  239. nthreads : int, default: -1
  240. passed to :func:`pyGDM2.core.scatter` and :func:`pyGDM2.linear.farfield`
  241. """
  242. def __init__(self, model, field_index=0,
  243. dir_index=[5], which_field='e_sc',
  244. kwargs_farfield=dict(Nteta=3, Nphi=5, tetamin=0, tetamax=np.pi/2.),
  245. consider_dS=False, averaging=True, absoluteI=False,
  246. kwargs_scatter={}, maximize=True, nthreads=-1):
  247. """constructor"""
  248. ## --- directionality problem setup
  249. if type(dir_index) not in (list, tuple, np.ndarray):
  250. self.dir_index = [dir_index]
  251. else:
  252. self.dir_index = dir_index
  253. if type(field_index) not in (list, tuple, np.ndarray):
  254. field_index = [field_index]
  255. else:
  256. field_index = field_index
  257. ## --- number of objectives
  258. if self.list_depth(self.dir_index) == 2:
  259. N_obj = len(self.dir_index)
  260. if len(field_index) == 1:
  261. field_index = [field_index[0]]*N_obj
  262. if len(field_index) != N_obj:
  263. raise ValueError("Number of 'field_index' entries must exactly match number of objectives!")
  264. elif self.list_depth(self.dir_index) == 1:
  265. N_obj = 1
  266. field_index = [field_index]
  267. self.dir_index = [self.dir_index]
  268. else:
  269. raise ValueError("Wrong shape of `dirindex` input parameter. Must be of 'depth' 1 or 2.")
  270. ## --- init base class
  271. super(self.__class__, self).__init__(model, field_index,
  272. maximize=maximize, nthreads=nthreads)
  273. self.which_field = which_field
  274. self.consider_dS = consider_dS
  275. self.averaging = averaging
  276. self.absoluteI = absoluteI
  277. self.kwargs_scatter = kwargs_scatter
  278. self.kwargs_farfield = kwargs_farfield
  279. self.dteta = ((kwargs_farfield['tetamax']-kwargs_farfield['tetamin']) /
  280. float(kwargs_farfield['Nteta']-1))
  281. self.dphi = 2.*np.pi/float(kwargs_farfield['Nphi']-1)
  282. def list_depth(self, L):
  283. """'depth' of a list"""
  284. depth = lambda L: isinstance(L, list) and max(map(depth, L))+1
  285. return depth(L)
  286. def objective_function(self, params):
  287. """evaluate directionality ratio"""
  288. self.model.generate_structure(params)
  289. ## --- main GDM simulation
  290. core.scatter(self.model.sim, nthreads=self.nthreads, verbose=0,
  291. **self.kwargs_scatter)
  292. ## --- iterate objective functions
  293. Iratios = []
  294. for i_obj, dir_idx in enumerate(self.dir_index):
  295. ## --- linear scattering to farfield, incoherent sum of all field_indices
  296. I_sc, I_tot, I0 = 0, 0, 0
  297. for di in self.field_index[i_obj]:
  298. tetalist, philist, _I_sc, _I_tot, _I0 = linear.farfield(
  299. self.model.sim, di,
  300. nthreads=self.nthreads,
  301. **self.kwargs_farfield)
  302. I_sc += _I_sc
  303. I_tot += _I_tot
  304. I0 += _I0
  305. if self.which_field.lower() == 'e0':
  306. I = I0
  307. elif self.which_field.lower() == 'e_sc':
  308. I = I_sc
  309. elif self.which_field.lower() == 'e_tot':
  310. I = I_tot
  311. else:
  312. raise ValueError("'which_field' must be one of ['e0', 'e_sc', 'e_tot']!")
  313. tetalist = tetalist.flatten()
  314. philist = philist.flatten()
  315. I = I.flatten()
  316. ## --- Processing (weighting, ratio-calc.)
  317. if self.consider_dS:
  318. ## --- weight intensities by solid angle of surface element
  319. dS = self.dteta*self.dphi*np.sin(tetalist)
  320. dS = dS + dS.max()*0.1 # slight cheating: increase all weights by 10% of overall max to not neglect 180degree backscattering
  321. else:
  322. dS = np.ones(tetalist.shape)
  323. ## --- compute either absolute intensity or directionality ratio
  324. if self.absoluteI:
  325. if self.averaging:
  326. Iratio = np.average(I[dir_idx]*dS[dir_idx])
  327. else:
  328. Iratio = np.sum(I[dir_idx]*dS[dir_idx])
  329. else:
  330. if self.averaging:
  331. non_idx = np.delete(range(len(I)), dir_idx)
  332. Iratio = (np.average(I[dir_idx]*dS[dir_idx]) /
  333. np.average(I[non_idx]*dS[non_idx]) )
  334. else:
  335. Iratio = (np.sum( (I[dir_idx]*dS[dir_idx])) /
  336. (np.sum(I*dS) - np.sum(I[dir_idx]*dS[dir_idx])) )
  337. Iratios.append(Iratio)
  338. return Iratios
  339. ## --- Problem description
  340. def get_extra_info(self):
  341. return "\n\tOptimize directionality of scattering."+\
  342. " Number of objectives: {}".format(len(self.dir_index))