"""Provides the :class:`Sample` class."""
from fibomat.linalg.boundingboxes import boundingbox
from fibomat.linalg.boundingboxes.dim_boundingbox import DimBoundingBox
from typing import Optional, List, Union, TypeVar, Type, Set
import re
import dataclasses
from fibomat.site import Site
from fibomat.linalg import DimVectorLike
from fibomat.backend import BackendBase, registry
from fibomat.utils import PathLike
from fibomat.shapes import DimShape
from fibomat.pattern import Pattern
from fibomat.layout import LayoutBase
from fibomat.default_backends import BokehBackend, StubRasterStyle
from fibomat.describable import Describable
@dataclasses.dataclass(frozen=True)
class _Annotation:
dim_shape: DimShape
filled: bool
color: Optional[str]
description: Optional[str]
BackendType = TypeVar('BackendType')
[docs]class Sample(Describable):
"""
This class is the glueing between all subcomponents of the library.
All shapes and their milling settings are added to this class and can be exported with the help of registered
backends .
"""
[docs] def __init__(self, *, description: Optional[str] = None):
"""
Args:
description (str, optional): Optional description of the project, default to None
"""
super().__init__(description)
self._sites: List[Site] = []
self._annotations: List[_Annotation] = []
[docs] def create_site(
self,
dim_position: DimVectorLike,
dim_fov: Optional[DimVectorLike] = None,
description: Optional[str] = None
) -> Site:
"""
Creates and Site in-place (hence, the Site is automatically added to the sample). Patterns can be added to the
returned object.
See :class:`fibomat.site.Site.__init__` for argument description.
Returns:
Site
"""
new_site: Site = Site(dim_position, dim_fov, description=description)
self._sites.append(new_site)
return new_site
[docs] def add_site(self, site_like: Site) -> None:
"""
Adds a Site to the project.
Alternatively, the '+=' operator can be used.
Args:
site_like (Site): new site
Returns:
None
"""
if isinstance(site_like, LayoutBase):
for site_ in site_like.layout_elements():
self._sites.append(site_)
else:
self._sites.append(site_like)
@property
def number_of_sites(self) -> int:
"""int: number of added sites"""
return len(self._sites)
@property
def bounding_box(self) -> Optional[DimBoundingBox]:
if not self._sites:
return None
else:
bbox = self._sites[0].bounding_box
for site in self._sites[1:]:
bbox = bbox.extended(site.bounding_box)
return bbox
def __iadd__(self, site_like):
"""See :meth:`~Sample.add_site`."""
self.add_site(site_like)
return self
@staticmethod
def _export(
backend_class: Type[BackendType], sites: Union[Site, List[Site]],
descr_pattern: Optional[Set[str]] = None,
**kwargs
) -> BackendType:
def _matches(description_) -> bool:
for pattern in descr_pattern:
if re.match(pattern, description_):
return True
return False
exporter: Type[BackendBase] = backend_class(**kwargs)
if isinstance(sites, Site):
if descr_pattern:
print('Warning: ignoring descriptions in _export for single site export.')
exporter.process_site(sites)
else:
for site_ in sites:
if descr_pattern:
description = site_.description
if description and _matches(description):
exporter.process_site(site_)
# descriptions.remove(description)
else:
exporter.process_site(site_)
# if descriptions:
# raise RuntimeError(f'Could not find sites with descriptions: {descriptions} in sample')
return exporter
[docs] def plot(self, show: bool = True, filename: Optional[PathLike] = None, descr_pattern: Optional[Set[str]] = None, **kwargs) -> BokehBackend:
"""
Plots and save the project using the :class:`~fibomat.default_backends.bokeh_backend.BokehBackend`.
Args:
show (bool): if true, the plot is opened in a browser automatically
filename (PathLike, optional): if filename is given, the plot is saved in this file. The file suffix should
be `*.htm` or `*.html`, default to None
`**kwargs`: parameters for the bokeh backend. These are directly passed to the __init__ method of the
BokehBackend class. The title parameter is automatically set to the :attr:`Sample.description`
Returns:
None
"""
plotter: BokehBackend = self._export(BokehBackend, self._sites, descr_pattern=descr_pattern, title=self._description, **kwargs)
for annot in self._annotations:
raster = StubRasterStyle(2) if annot.filled else StubRasterStyle(1)
plotter.process_pattern(Pattern(
annot.dim_shape, None, raster, _annotation=True, _color=annot.color, description=annot.description
))
plotter.plot()
if filename:
plotter.save(filename)
if show:
plotter.show()
return plotter
[docs] def export(self, exp_backend: Union[str, Type[BackendBase]], **kwargs) -> BackendBase:
"""
Exports the project. Note that the method returns the backend object so you will be able to save a file or show
a plot. See backends example nd docs for details.
.. note:: The export method does not save any files on its one. This must be done by the user manually. See docs
of the used backend for details.
Args:
exp_backend (str or Type[backend.BackendBase]):
name of the backend or class. The backend must be registered before.
**kwargs: optional arguments are passed to the backend's __init__ method
Returns:
BackendBase
"""
if isinstance(exp_backend, str):
exp_backend = registry.get(exp_backend)
return self._export(exp_backend, self._sites, description=self._description, **kwargs)
[docs] def export_multi(self, exp_backend: Union[str, Type[BackendBase]], **kwargs) -> List[BackendBase]:
"""
Similar to :meth:`Project.export` but for each :class:`fibomat.site.Site` an individual backend instance is
returned.
This can be usefull if multiple sites are used within fibomat but the pattern system only supports one site at a
time.
Returns:
List[BackendBase]
"""
backends: List[BackendBase] = []
if isinstance(exp_backend, str):
exp_backend = registry.get(exp_backend)
for added_site in self._sites:
backends.append(self._export(exp_backend, added_site, description=self._description, **kwargs))
return backends
[docs] def export_with_description(
self, exp_backend: Union[str, Type[BackendBase]], descr_pattern: Set[str], **kwargs
) -> BackendBase:
"""
Exports the project. Note that the method returns the backend object so you will be able to save a file or show
a plot. See backends example nd docs for details.
Only sites with a description given in `descriptions` will be exported. An exception is raised, if no site is
found with a given description.
.. note:: The export method does not save any files on its one. This must be done by the user manually. See docs
of the used backend for details.
Args:
exp_backend (str or Type[backend.BackendBase]):
name of the backend or class. The backend must be registered before.
descr_pattern (Set[str]): a set of descriptions of the sites which should be exported.
**kwargs: optional arguments are passed to the backend's __init__ method
Returns:
BackendBase
"""
if isinstance(exp_backend, str):
exp_backend = registry.get(exp_backend)
return self._export(exp_backend, self._sites, description=self._description, descr_pattern=descr_pattern, **kwargs)
[docs] def add_annotation(
self, dim_shape: DimShape, filled: bool = False, color: Optional[str] = None, description: Optional[str] = None
) -> None:
"""
Add `dim_shape` to a annotation layer. This layer is only used to visualize extra shapes and is ignored by the
exporting backend.
Args:
dim_shape (DimShape): shape
filled (bool): If True, shape is plotted filled (only possible if shape is closed)
color (str, Optional): a color bokeh can understand, default to None
description (str, optional): description, default to None
Returns:
None
"""
self._annotations.append(_Annotation(dim_shape=dim_shape, filled=filled, color=color, description=description))