Source code for geoptics.elements.scene

# -*- coding: utf-8 -*-

# Copyright (C) 2014 ederag <edera@gmx.fr>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with GeOptics; see the file LICENSE.txt.  If not, see
# <http://www.gnu.org/licenses/>.


"""Define a Scene, and hold all the propagation calculations."""


import logging
logger = logging.getLogger(__name__)   # noqa: E402

from geoptics.elements import rays
from geoptics.elements import regions
from geoptics.elements import sources
from geoptics.elements.rays import Ray
from geoptics.elements.regions import Region
from geoptics.elements.sources import Source
from geoptics.shared.tools import find_classes


[docs]class Scene(object): """Scene holding all items.""" def __init__(self): # FIXME: duplicated code with guis.qt.scene # correspondance between names in dumped data and classes self.class_map = {'Rays': find_classes(rays), 'Sources': find_classes(sources), 'Regions': find_classes(regions)} #: background medium (air by default) self.background = regions.Region(n=1.0) #: list of all regions, excluding background self.regions = [] #: list of all sources self.sources = []
[docs] def add(self, other): """Add an element to the scene. Args: other: the element to add to the scene. The element can be a :class:`~.elements.sources.Source` or a :class:`~.elements.regions.Region`. A :class:`~.elements.rays.Ray` is also accepted, for internal use, but normal users should only add Sources or Regions. """ if isinstance(other, Ray): raise TypeError("Always use a source to create a ray.\n" "For instance, SingleRay()") elif isinstance(other, Region): if other in self.regions: raise ValueError("Region already in scene") else: self.regions.append(other) elif isinstance(other, Source): if other in self.sources: raise ValueError("Source already in scene") else: self.sources.append(other) elif isinstance(other, dict): self._add_config(other) else: raise NotImplementedError("Trying to add {}".format(type(other)))
[docs] def _add_config(self, config): """Add regions and sources from a configuration dictionary.""" for cls, item_config in self._add_iterator(config): # with the "scene=" keyword, this new instance # will automatically be added to scene cls.from_config(item_config, scene=self)
[docs] def _add_iterator(self, config): """Return an iterator over the available items in config.""" if ('Regions' in config) or ('Sources' in config): for category in ('Regions', 'Sources'): for item_config in config[category]: name = item_config['Class'] cls = self.class_map[category][name] yield cls, item_config elif 'Class' in config: item_config = config name = item_config['Class'] cls = None for category in ('Regions', 'Sources'): if name in self.class_map[category]: cls = self.class_map[category][name] if cls is None: logger.error("{} class not found".format(name)) raise KeyError() yield cls, item_config else: logger.error("neither 'Regions', 'Sources', nor 'Class' found") raise KeyError()
[docs] def clear(self): """Remove all regions and sources.""" # do not set empty lists, # otherwise guis would not remove items from display # working on "reversed" iterators allow to "pop" items # starting from the end of the list, avoiding side effects # Note: there are unnecessary lookups in self.remove() # should we have a "pop" method too ? for region in reversed(self.regions): self.remove(region) for source in reversed(self.sources): self.remove(source) logger.debug("scene cleared")
@property def config(self): # noqa: D401 """Configuration dictionary.""" return {'Regions': [region.config for region in self.regions], 'Sources': [source.config for source in self.sources], } @config.setter def config(self, config): self.clear() self._add_config(config) logger.debug("config set")
[docs] @classmethod def from_config(cls, config): """Alternate constructor. Args: config (dict): Configuration dictionary Returns: :class:`.Scene`: new Scene instance """ scene = cls() scene.config = config return scene
[docs] def remove(self, other): """Remove element from scene.""" if isinstance(other, Ray): raise TypeError("scene: Rays should be removed only from their Source") elif isinstance(other, Region): self.regions.remove(other) elif isinstance(other, Source): self.sources.remove(other) else: raise NotImplementedError("Trying to remove {}".format(type(other)))
[docs] def propagate(self, rays=None): """Propagate rays from sources, across regions.""" if rays is None: # by default propagate all rays rays = [ray for source in self.sources for ray in source.rays] for ray in rays: ray.propagate(self)
[docs] def region_at(self, *args, **kwargs): """Return the region where the given point belongs to. In case of overlapping `self.regions`, return the first one found. Args: same as meth:`geoptics.elements.regions.Region.contains` Returns: Region: the region `point` belongs to. (or `self.background` if inside no region) """ for region in self.regions: if region.contains(*args, **kwargs): return region # no match return self.background