Source code for wcc_etc.scene

import os
import warnings

import numpy as np
from astropy import units as u
from synphot import Observation, SourceSpectrum, SpectralElement
from synphot import units as su
from synphot.models import (
    BlackBodyNorm1D,
    ConstFlux1D,
    Empirical1D,
    GaussianFlux1D,
    PowerLawFlux1D,
)

from .meta import _MetaHolder_

__all__ = ["get_scene", "get_scene_from_file", "get_scene_element", "Scene"]

# Magnitude-system resolution: 'abmag' lives in astropy.units, 'vegamag' in
# synphot.units. getattr(u, ...) cannot see VEGAMAG, so resolve explicitly.
_MAGSYS = {"abmag": u.ABmag, "vegamag": su.VEGAMAG}


def _resolve_magsys(magsys):
    """Resolve a magnitude-system spec to an astropy/synphot unit.

    Parameters
    ----------
    magsys : str or Unit
        Case-insensitive ``'abmag'`` or ``'vegamag'``, or a unit object
        (anything exposing ``is_equivalent``), which is returned unchanged.

    Returns
    -------
    Unit
        ``astropy.units.ABmag`` or ``synphot.units.VEGAMAG``.

    Raises
    ------
    ValueError
        If ``magsys`` is an unrecognized string.
    """
    if hasattr(magsys, "is_equivalent"):
        return magsys
    try:
        return _MAGSYS[str(magsys).lower()]
    except KeyError:
        raise ValueError(
            f"unknown magsys {magsys!r}; expected one of {sorted(_MAGSYS)} "
            "or an astropy/synphot magnitude unit"
        )


_VEGA = None


def _get_vega():
    """Lazily load and cache the Vega reference spectrum for VEGAMAG work."""
    global _VEGA
    if _VEGA is None:
        _VEGA = SourceSpectrum.from_vega()
    return _VEGA


# ===================== #
#  Parametric spectra   #
# ===================== #
# Reserved spectrum names that build a synphot SourceSpectrum from parameters
# carried in a SceneElement's meta (see SceneElement.set_spectrum).


def _build_blackbody(meta):
    """Blackbody spectrum. Requires meta['teff'] in K."""
    teff = meta.get("teff")
    if teff is None:
        raise ValueError("blackbody source requires 'teff' (in K)")
    return SourceSpectrum(BlackBodyNorm1D, temperature=teff)


def _build_flat(meta):
    """Flat spectrum. meta['flat_unit'] is 'fnu' (default, AB-flat) or 'flam'.

    The absolute amplitude is arbitrary because the spectrum is normalized to
    a magnitude downstream; only the flat-in-F_nu vs flat-in-F_lambda shape
    matters here.
    """
    flat_unit = str(meta.get("flat_unit", "fnu")).lower()
    if flat_unit == "fnu":
        amplitude = 1 * u.Jy
    elif flat_unit == "flam":
        amplitude = 1 * su.FLAM
    else:
        raise ValueError(f"unknown flat_unit={flat_unit!r}; use 'fnu' or 'flam'")
    return SourceSpectrum(ConstFlux1D, amplitude=amplitude)


def _build_powerlaw(meta):
    """Power-law spectrum: F_lambda proportional to (lambda/lambda_ref)**alpha.

    Requires meta['alpha']; meta['lambda_ref'] defaults to 5500 A. Note synphot's
    PowerLawFlux1D uses (x/x_0)**(-alpha), so the synphot alpha is negated to make
    the public 'alpha' the F_lambda exponent.
    """
    alpha = meta.get("alpha")
    if alpha is None:
        raise ValueError("powerlaw source requires 'alpha'")
    lambda_ref = meta.get("lambda_ref", 5500.0)
    return SourceSpectrum(
        PowerLawFlux1D, amplitude=1 * su.FLAM, x_0=lambda_ref * u.AA, alpha=-alpha
    )


def _build_emission(meta):
    """Sum of Gaussian emission lines with absolute integrated flux.

    Requires meta['lines'], a list of dicts with keys 'wave' (A), 'flux'
    (integrated line flux in erg/s/cm^2) and optional 'fwhm' (A, default 2).
    Intended to be used with mag=None so the absolute flux is preserved.
    """
    lines = meta.get("lines")
    if not lines:
        raise ValueError("emission source requires a non-empty 'lines' list")
    spectrum = None
    for line in lines:
        gauss = SourceSpectrum(
            GaussianFlux1D,
            total_flux=line["flux"] * u.erg / u.s / u.cm**2,
            mean=line["wave"] * u.AA,
            fwhm=line.get("fwhm", 2.0) * u.AA,
        )
        spectrum = gauss if spectrum is None else spectrum + gauss
    return spectrum


def _get_unit(unit):
    """Resolve astropy or synphot unit names."""
    if hasattr(unit, "is_equivalent"):
        return unit
    if hasattr(su, str(unit)):
        return getattr(su, str(unit))
    return u.Unit(unit)


def _get_file_column(data, column, label):
    if data.dtype.names is not None:
        if isinstance(column, str):
            return data[column]
        return data[data.dtype.names[int(column)]]

    if isinstance(column, str):
        raise ValueError(
            f"{label}_column={column!r} requires names=True or a named table"
        )
    return np.asarray(data)[:, int(column)]


def _build_file(meta):
    """Empirical spectrum read from wavelength and flux columns in a text file.

    Requires ``source_file`` (or ``file``/``filename``). By default, column 0 is
    wavelength in Angstrom and column 1 is flux in FLAM. CSV delimiters are
    inferred from a ``.csv`` extension; otherwise whitespace-separated input is
    assumed. Pass string column names with ``names=True`` for named tables.
    """
    filename = meta.get("source_file", meta.get("file", meta.get("filename")))
    if filename is None:
        raise ValueError("file source requires 'source_file' (or 'file'/'filename')")
    delimiter = meta.get("delimiter")
    if delimiter is None and str(filename).lower().endswith(".csv"):
        delimiter = ","

    wave_column = meta.get("wave_column", meta.get("wavelength_column", 0))
    flux_column = meta.get("flux_column", 1)
    names = meta.get("names")
    if names is None:
        names = isinstance(wave_column, str) or isinstance(flux_column, str)
    if names is False:
        names = None

    data = np.genfromtxt(
        filename,
        delimiter=delimiter,
        names=names,
        comments=meta.get("comments", "#"),
        dtype=float,
    )
    wave = np.atleast_1d(_get_file_column(data, wave_column, "wave")).astype(float)
    flux = np.atleast_1d(_get_file_column(data, flux_column, "flux")).astype(float)

    if wave.size != flux.size:
        raise ValueError(
            "file source wavelength and flux columns must have the same length"
        )
    finite = np.isfinite(wave) & np.isfinite(flux)
    wave = wave[finite]
    flux = flux[finite]
    if wave.size < 2:
        raise ValueError(
            "file source requires at least two finite wavelength/flux rows"
        )

    order = np.argsort(wave)
    wave = wave[order] * _get_unit(meta.get("wave_unit", "AA"))
    flux = flux[order] * _get_unit(meta.get("flux_unit", "FLAM"))
    return SourceSpectrum(Empirical1D, points=wave, lookup_table=flux)


_SPECTRUM_BUILDERS = {
    "blackbody": _build_blackbody,
    "flat": _build_flat,
    "powerlaw": _build_powerlaw,
    "emission": _build_emission,
    "file": _build_file,
}

# Shape-defining parameters each parametric spectrum accepts. These become
# updatable for a source of that type (see SceneElement.mutable_parameters);
# changing one rebuilds the spectrum. The spectrum *type* itself is not here,
# so it cannot be changed after creation.
_SPECTRUM_PARAMS = {
    "blackbody": ["teff"],
    "flat": ["flat_unit"],
    "powerlaw": ["alpha", "lambda_ref"],
    "emission": ["lines"],
    "file": [
        "source_file",
        "file",
        "filename",
        "wave_column",
        "wavelength_column",
        "flux_column",
        "wave_unit",
        "flux_unit",
        "delimiter",
        "names",
        "comments",
    ],
}

# Top level


[docs] def get_scene( name, mag, host=None, host_prop={}, background="zodi", background_prop={}, **kwargs ): """ Get a Scene object with a source, an optional host, and an optional background. Parameters ---------- name : str Name of the source spectrum. mag : float Magnitude of the source. host : str, optional Name of the host spectrum. Default is None. host_prop : dict, optional Properties for the host spectrum. Default is {}. background : str, optional Name of the background spectrum. Default is "zodi". background_prop : dict, optional Properties for the background spectrum. Default is {}. **kwargs Additional keyword arguments passed to get_scene_element for the source. Returns ------- Scene The initialized Scene object. """ source = get_scene_element(name, mag=mag, **kwargs) if host is None and not host_prop: host = None else: host = get_scene_element(host, **host_prop) if background is None and not background_prop: background = None else: background = get_scene_element(background, **background_prop) return Scene(source=source, host=host, background=background)
[docs] def get_scene_from_file( source_file, mag=None, wave_column=0, flux_column=1, wave_unit="AA", flux_unit="FLAM", host=None, host_prop=None, background="zodi", background_prop=None, **kwargs, ): """ Build a Scene whose source spectrum is read from a wavelength/flux file. Parameters ---------- source_file : str Path to a text file containing wavelength and flux columns. mag : float or None, optional Magnitude used to normalize the source. If None, the file's absolute flux is preserved. wave_column, flux_column : int or str, optional Wavelength and flux columns. String columns require a named table or ``names=True``. wave_unit, flux_unit : str or Unit, optional Units for the wavelength and flux columns. Defaults are Angstrom and FLAM. **kwargs Additional keyword arguments passed to get_scene. Useful file-reading options include ``delimiter``, ``names``, and ``comments``. Returns ------- Scene A scene with a file-backed source spectrum. """ return get_scene( "file", mag=mag, source_file=source_file, wave_column=wave_column, flux_column=flux_column, wave_unit=wave_unit, flux_unit=flux_unit, host=host, host_prop=host_prop, background=background, background_prop=background_prop, **kwargs, )
[docs] def get_scene_element(element=None, **kwargs): """ Generic top level function to instanciate a scene element. Parameters ---------- element : str, dict, or None, optional Flexible variable to help instantiating a scene element. - If str: name of a specific pre-defined entry, like 'zodi'. - If dict: configuration that will supersede default config. It overrides kwargs entries. - If None: not used, all rely on kwargs. Example equivalent calls: - get_scene_element("G5IV", mag=20) - get_scene_element({"name": "G5IV", "mag": 20}) - get_scene_element(name="G5IV", mag=20) **kwargs Configuration parameters used as SceneElement.from_config(kwargs). Returns ------- SceneElement The loaded scene element. """ if type(element) is str: name = kwargs["name"] = element elif type(element) is dict: kwargs |= element name = kwargs.get("name", kwargs.get("spectrum", None)) # build default configuration given names. ## Zodi if name is not None and name in ["zodi", "zodiacal", "background"]: default_config = { "mag": 22.5, "surface_brightness": True, "bandpass": "johnson_v", } ## anything else else: default_config = { "mag": 21, "surface_brightness": False, "bandpass": "johnson_v", } return SceneElement.from_config((default_config | kwargs))
# ============= # # Source # # ============= # def broadcast_mapping(value, ntargets): """ Broadcast a value to a given number of targets. Parameters ---------- value : array_like Value(s) to be broadcasted. ntargets : int Number of targets to broadcast to. Returns ------- ndarray The broadcasted values. """ value = np.atleast_1d(value) if np.ndim(value) > 1: # squeeze drop useless dimensions. broadcasted_values = np.broadcast_to(value, (ntargets, value.shape[-1])) else: broadcasted_values = np.broadcast_to(value, ntargets) return broadcasted_values # ============= # # Source # # ============= # class SceneElement(_MetaHolder_): """ A class representing a single element of a scene (source, host, or background). Attributes ---------- spectrum : SourceSpectrum The spectrum of the element. mag : Quantity The magnitude of the element. band : SpectralElement The bandpass used for magnitude normalization. mag_is_surface_brightness : bool Whether the magnitude is defined per unit area. """ # NOTE: "spectrum" is deliberately absent — the source *type* is fixed at # creation. Type-specific shape parameters are added in mutable_parameters. _mutable_parameters = ["mag", "magsys", "bandpass", "surface_brightness"] def __init__( self, spectrum, mag, magsys="vegamag", bandpass="johnson_v", surface_brightness=False, meta={}, ): """ Initialize a SceneElement. Parameters ---------- spectrum : str or SourceSpectrum The spectrum or path to a spectrum file. mag : float The magnitude of the element. magsys : str, optional The magnitude system: 'vegamag' or 'abmag' (case-insensitive). Default is "vegamag". bandpass : str or SpectralElement, optional The bandpass filter. Default is "johnson_v". surface_brightness : bool, optional Whether the magnitude is surface brightness (mag/arcsec^2). Default is False. meta : dict, optional Additional metadata. Default is {}. """ input_parameters = { key: value for key, value in locals().items() if key not in ["self", "meta"] and value is not None and not key.startswith("__") } super().__init__(meta | input_parameters) self.set_spectrum(spectrum) @classmethod def from_config(cls, config): """ Create a SceneElement from a configuration dictionary. Parameters ---------- config : dict Configuration dictionary containing 'spectrum' (or 'name'), 'mag', and optionally 'magsys', 'bandpass', 'surface_brightness'. Returns ------- SceneElement The initialized SceneElement. """ # make sure these keys exist if "name" in config: config["spectrum"] = config.pop("name") input_kwargs = {key: config.get(key) for key in ["spectrum", "mag"]} input_kwargs |= { key: config.get(key) for key in ["magsys", "bandpass", "surface_brightness"] if key in config } # else default as given by __init__ return cls(meta=config, **input_kwargs) # =========== # # methods # # =========== # def set_spectrum(self, spec_or_file): """ Generic setter for any source component. Parameters ---------- spec_or_file : str or SourceSpectrum or None The spectrum, a path to a spectrum file, or a spectral type name. """ # make sure you have a spectrum. if type(spec_or_file) in [str]: if spec_or_file.lower() in _SPECTRUM_BUILDERS: # parametric spectrum (blackbody, flat, powerlaw, emission) # built from parameters carried in self.meta. spectrum = _SPECTRUM_BUILDERS[spec_or_file.lower()](self.meta) else: if not os.path.isfile(spec_or_file): # may that is a spectral type: from .io import get_any_astro_name spec_or_file = get_any_astro_name(spec_or_file) spectrum = SourceSpectrum.from_file(spec_or_file) # the or None enables to switch of the host by setting it to None elif isinstance(spec_or_file, SourceSpectrum) or spec_or_file is None: spectrum = spec_or_file self._spectrum = spectrum def update(self, reset=False, **kwargs): """ Update mutable parameters, rebuilding the spectrum if a shape parameter changed. The spectrum *type* is fixed at creation, so ``spectrum`` is not mutable. Updating a type-specific shape parameter (e.g. ``teff``, ``alpha``, ``lines``) rebuilds the underlying synphot spectrum; ``mag`` / ``bandpass`` / etc. take effect at ``get_spectrum`` time and need no rebuild. """ updated = super().update(reset=reset, **kwargs) name = self.meta.get("spectrum") shape_params = ( _SPECTRUM_PARAMS.get(name.lower(), []) if isinstance(name, str) else [] ) if any(key in shape_params for key in updated): self.set_spectrum(name) return updated # ------- # # GETTER # # ------- # def get_mag(self, area=None): """ Return the actual magnitude, accounting for the area if mag is a surface brightness. Parameters ---------- area : float or Quantity, optional The area in arcsec^2. Required if mag_is_surface_brightness is True. Returns ------- Quantity The magnitude. """ if self.mag_is_surface_brightness: # must work in float (not astropy unit) because of mag and np.log. ## area should be in arcsec**2 if "astropy.units" in str(type(area)): area_arcsec2 = area.to("arcsec**2").value else: # if float, assuming it is in arcsec2 area_arcsec2 = area ## take the value of mag mag_per_arcsec2 = self.mag.value mag_unit = self.mag.unit mag = (mag_per_arcsec2 - 2.5 * np.log10(area_arcsec2)) * mag_unit else: mag = self.mag return mag def get_spectrum(self, apply_mag=True, as_array=False, area=None): """ Get the spectrum of the element. Parameters ---------- apply_mag : bool, optional Whether to normalize the spectrum to the stored magnitude. Default is True. as_array : bool, optional Whether to return the spectrum as a wavelength/flux array pair. Default is False. area : float or Quantity, optional The area for surface brightness normalization. Default is None. Returns ------- SourceSpectrum or tuple The spectrum or (wavelength, flux) tuple if as_array is True. """ if isinstance(self.spectrum, SourceSpectrum): mag = self.get_mag(area=area) if apply_mag else None if apply_mag and mag is not None: if mag.unit == su.VEGAMAG: spectrum = self.spectrum.normalize( mag, band=self.band, vegaspec=_get_vega() ) else: spectrum = self.spectrum.normalize(mag, band=self.band) else: # mag is None -> spectrum already carries absolute flux # (e.g. emission-line sources); pass it through unchanged. spectrum = self.spectrum else: warnings.warn(f"cannot get the spectrum of the stored {self.spectrum=}") return None if as_array: spectrum = spectrum._get_arrays(None) return spectrum def get_observation(self, band=None, area=None): """ Get a synphot Observation of the element. Parameters ---------- band : SpectralElement, optional The bandpass filter. Defaults to the element's stored band. area : float or Quantity, optional The area for surface brightness normalization. Default is None. Returns ------- Observation or None The synphot Observation. """ if band is None: band = self.band spectrum = self.get_spectrum(apply_mag=True, as_array=False, area=area) if spectrum is None: return None return Observation(spectrum, band, force="extrap") def show(self, ax=None, apply_mag=True, **kwargs): """ Show the spectrum or host flux. Parameters ---------- ax : matplotlib.axes.Axes, optional Axes to plot on. If None, a new figure is created. apply_mag : bool, optional Whether to apply magnitude normalization. Default is True. **kwargs Keyword arguments passed to ax.plot. Returns ------- matplotlib.figure.Figure The figure object. """ if ax is None: import matplotlib.pyplot as plt fig, ax = plt.subplots(figsize=(7, 3)) else: fig = ax.figure # data to show lbda, flux = self.get_spectrum(as_array=True, apply_mag=apply_mag) # plot ax.plot(lbda, flux, **kwargs) ax.set_xlabel("Wavelength [A]") ax.set_ylabel("Flux") return fig # ------------ # # internal # # ------------ # def _parse_mag_(self): """ Parse the magnitude from metadata. Returns ------- Quantity The parsed magnitude with units. """ # magnitude mag = self.meta.get("mag", None) if mag is None: # explicit None is a valid "no normalization" sentinel (e.g. # absolute-flux emission sources); only warn if it is truly missing. if "mag" not in self.meta: warnings.warn("not mag in self.meta") else: # make sure mag has the correct units. magsys = _resolve_magsys(self.meta.get("magsys", "vegamag")) mag = mag * magsys return mag def _parse_band_(self): """ Parse the bandpass from metadata. Returns ------- SpectralElement The parsed bandpass. Raises ------ ValueError If the band metadata is neither a string nor a SpectralElement. """ # band or bandpass accepted band = self.meta.get("band", self.meta.get("bandpass")) if type(band) is str: from .io import resolve_bandpass band = resolve_bandpass(band) elif not isinstance(band, SpectralElement): raise ValueError(f"{band=} meta is neither a str nor a SpectralElement.") return band # ============= # # Properties # # ============= # @property def mutable_parameters(self): """ Parameters that can be updated. Always the common normalization parameters, plus the shape parameters specific to this source's spectrum type (e.g. ``teff`` for a blackbody). The spectrum *type* itself is fixed at creation and is not included. """ params = list(self._mutable_parameters) name = self.meta.get("spectrum") if isinstance(name, str): params += _SPECTRUM_PARAMS.get(name.lower(), []) return params @property def spectrum(self): """ The source spectrum. """ return self._spectrum @property def mag(self): """ The magnitude as an astropy Quantity. """ return self._parse_mag_() @property def band(self): """ The bandpass as a synphot SpectralElement. """ return self._parse_band_() @property def mag_is_surface_brightness(self): """ Boolean indicating if the magnitude is a surface brightness. """ return self.meta.get("surface_brightness", False)
[docs] class Scene(_MetaHolder_): """ A collection of SceneElements (source, host, background) representing a full observation scene. Attributes ---------- source : SceneElement The main source of interest. host : SceneElement The host galaxy or environment. background : SceneElement The sky background. """ # ZODI TO BE IMPLEMENTED # self.background = self.config['zodi']['zodi_mag_r'] _mutable_parameters = [] def __init__(self, source=None, host=None, background=None, meta={}): """ Initialize a Scene object. Parameters ---------- source : SceneElement, optional The source element. Default is None. host : SceneElement, optional The host element. Default is None. background : SceneElement, optional The background element. Default is None. meta : dict, optional Additional metadata. Default is {}. """ self._source = source self._host = host self._background = background super().__init__(meta)
[docs] @classmethod def from_config(cls, config): """ Create a Scene from a configuration dictionary. Parameters ---------- config : dict Configuration dictionary containing 'source', 'host', and 'background' keys, each being a configuration for SceneElement.from_config. Returns ------- Scene The initialized Scene. """ # Source if source_config := config.get("source", {}): source = SceneElement.from_config(source_config) else: source = None # host if host_config := config.get("host", {}): host = SceneElement.from_config(host_config) else: host = None # background if background_config := config.get("background", {}): background = SceneElement.from_config(background_config) else: background = None # And build it. return cls(source=source, host=host, background=background, meta=config)
# ============= # # methods # # ============= #
[docs] def reset(self): """ Reset each element in the scene. """ # reset each element _ = self.call_down("reset", which="all")
[docs] def update(self, reset=False, **kwargs): """ Update scene elements using double-underscore prefixed keywords. Parameters ---------- reset : bool, optional Not currently used. Default is False. **kwargs Keywords like 'source__mag=20' or 'background__mag=22'. Returns ------- list List of updated keys. """ update_source = {} update_host = {} update_background = {} updated_key = [] for key, value in kwargs.items(): # does not matter if value is None: continue if key.startswith("source__"): update_source[key.replace("source__", "")] = value elif key.startswith("host__"): update_host[key.replace("host__", "")] = value elif key.startswith("background__"): update_background[key.replace("background__", "")] = value else: warnings.warn("cannot parse {key=} ; should start by 'source__' etc.") continue updated_key.append(key) if self.has_source(): self.source.update(**update_source) if self.has_host(): self.host.update(**update_host) if self.has_background(): self.background.update(**update_background) return updated_key
[docs] def get_elements(self, which="*", as_dict=False): """ Get specified scene elements. Parameters ---------- which : str or list, optional Which elements to get ('*', 'all', 'source', 'host', 'background'). Default is "*". as_dict : bool, optional Whether to return elements as a dictionary. Default is False. Returns ------- list or dict The requested scene elements. """ if which in ["*", "all"]: which = [ element_ for element_ in self.element_names if getattr(self, element_) is not None ] else: which = np.atleast_1d(which) values = [ element_obj for which_ in self.element_names if which_ in which and (element_obj := getattr(self, which_)) is not None ] if as_dict: return dict(zip(which, values)) return values
[docs] def get_mag(self, area=None, which="*", as_dict=False): """ Get magnitude for specified scene elements. Parameters ---------- area : float or Quantity, optional The area for surface brightness normalization. Default is None. which : str or list, optional Which elements to get magnitudes for. Default is "*". as_dict : bool, optional Whether to return as a dictionary. Default is False. Returns ------- list or dict The magnitudes. """ return self.call_down("get_mag", area=area, which=which, as_dict=as_dict)
[docs] def get_spectrum(self, area=None, apply_mag=True, which="*", as_dict=False): """ Get spectrum for specified scene elements. Parameters ---------- area : float or Quantity, optional The area for surface brightness normalization. Default is None. apply_mag : bool, optional Whether to normalize the spectrum. Default is True. which : str or list, optional Which elements to get spectra for. Default is "*". as_dict : bool, optional Whether to return as a dictionary. Default is False. Returns ------- list or dict The spectra. """ return self.call_down( "get_spectrum", area=area, apply_mag=apply_mag, which=which, as_dict=as_dict )
[docs] def get_observation(self, band=None, area=None, which="*", as_dict=False): """ Get observation for specified scene elements. Parameters ---------- band : SpectralElement, optional The bandpass filter. Default is None. area : float or Quantity, optional The area for surface brightness normalization. Default is None. which : str or list, optional Which elements to get observations for. Default is "*". as_dict : bool, optional Whether to return as a dictionary. Default is False. Returns ------- list or dict The observations. """ return self.call_down( "get_observation", band=band, area=area, which=which, as_dict=as_dict )
[docs] def has_source(self): """ Check if the scene has a source element. Returns ------- bool """ return self._source is not None
[docs] def has_host(self): """ Check if the scene has a host element. Returns ------- bool """ return self._host is not None
[docs] def has_background(self): """ Check if the scene has a background element. Returns ------- bool """ return self._background is not None
# -------- # # GETTER # # -------- # # ----------- # # Internal # # ----------- #
[docs] def call_down( self, what, mapargs=None, allow_call=True, which="*", as_dict=False, **kwargs ): """ Call a method on each target element in the collection. Parameters ---------- what : str The name of the attribute or method to access on each element. mapargs : array_like, optional Positional arguments to be mapped to each element call. Default is None. allow_call : bool, optional If True and the attribute is callable, call it. Default is True. which : str or list, optional Which elements to include. Default is "*". as_dict : bool, optional Whether to return results as a dictionary. Default is False. **kwargs Keyword arguments passed to the method call. Returns ------- list or dict The results from each element. """ # applied to target.simulation elements = self.get_elements(which=which, as_dict=as_dict) if as_dict: element_names = elements.keys() elements = elements.values() if mapargs is not None: mapargs = broadcast_mapping(mapargs, elements) values = [ getattr(element, what)(maparg_, **kwargs) for maparg_, element in zip(mapargs, elements) ] else: values = [ attr if not (callable(attr := getattr(element, what)) and allow_call) else attr(**kwargs) for element in elements ] if as_dict: return dict(zip(element_names, values)) return values
# -------- # # PLOTTER # # -------- # @property def source(self): """ The source SceneElement. """ return self._source @property def host(self): """ The host SceneElement. """ return self._host @property def background(self): """ The background SceneElement. """ return self._background @property def element_names(self): """ Names of the potential elements in a scene. """ return ["source", "host", "background"] @property def meta(self): """ Combined metadata for the scene and its elements. """ return self._meta | { element_name: element.meta for element_name in self.element_names if (element := getattr(self, element_name)) is not None } @property def mutable_parameters(self): """ List of parameters that can be updated. """ return self._mutable_parameters + [ f"{element_name}__{k}" for element_name in self.element_names if (element := getattr(self, element_name)) is not None for k in element.mutable_parameters ]