Class SpecsynLibChained
Defined in File SpecsynLibChained.hpp
Inheritance Relationships
Base Type
public specsyn::Specsyn(Class Specsyn)
Class Documentation
-
class SpecsynLibChained : public specsyn::Specsyn
A Specsyn specialization that chains together several SpecsynLib libraries.
Some parts of stellar parameter space are best covered by different spectral libraries – for example, an OB-star library like TLUSTY for hot, massive stars, and a library like BOSZ for cooler, lower-mass stars. This class lets several such libraries be combined into a single Specsyn: each one is tried in turn, in priority order, and the first one able to produce a spectrum for a given star is used.
Chained libraries need not share a single native wavelength grid, so the constructor builds a common grid spanning all of them (see makeCommonWlGrid) and resamples every library onto it (see SpecsynLib::resample) before storing them.
Public Functions
-
SpecsynLibChained(const std::vector<std::string> &spectraName, double fehMin, double fehMax, double afe, double cfe, const std::vector<double> µTurb, double r, const std::string ®istryName, double wlMin, double wlMax, std::size_t nWl, bool tClamp, const io::SimControls &controls)
Construct a SpecsynLibChained from a list of spectral libraries on disk.
) according to its own registry entry – WR_grid = true, WD_grid = true, or neither – so within a given GridType, if a star’s parameters fall within more than one of that type’s own libraries’ grids, the spectrum is generated by whichever one appears earliest in spectraName.
) as a flag to fall back on each library’s own native wavelength coverage
Constructs one library per entry of spectraName, in order – a SpecsynLibWR for an entry whose registry entry has WR_grid = true, a SpecsynLibWD for an entry with WD_grid = true (mutually exclusive with WR_grid), a SpecsynLibNoWind for every other entry – pairing each one with the corresponding entry of microTurb (or that library’s own registry default, if microTurb is empty; ignored entirely for a SpecsynLibWR or SpecsynLibWD entry, neither of which has a microTurb axis) and otherwise using the remaining arguments – including wlMin, wlMax, and nWl – unchanged for each one. Every library, with no exception, is constructed with OOBPolicy::coerce, so a star outside its grid (or in one of its gaps) is still handled by that same library if it has at least one valid neighboring grid point, and falls through to the next library in its own chain only if it truly has none – see spec()’s own comment for what happens once a star falls through its entire chain. Each is upcast to (and stored as) the plain Specsyn it was constructed as a SpecsynLib<OOBPolicy::coerce> specialization of, since every function this class actually needs on it from that point on – spec(), specForce() – lives on that common base.Once every library has been loaded on its own native wavelength grid, makeCommonWlGrid is used to build a single grid spanning all of them, and each library is resampled (see SpecsynLib::resample) onto that common grid, so that every chained library’s spec() output shares the same length and wavelength values – and so that wl() correctly describes every one of them, not just the first. Only after this are the libraries sorted into wrLibs_/wdLibs_/ normalLibs_, per their own already-known GridType, still in their original relative order within spectraName.
logTeffMin_/logTeffMax_/loggMin_/loggMax_ are unconditionally filled with quiet_NaN() first. If tClamp is true, also scans every chained library’s own log(Teff) grid (via its logTeff() accessor) for its minimum and maximum, storing them in logTeffMin_[t]/logTeffMax_[t] (see updateLogTeffRange), where t is whichever GridType that library actually is (SpecsynLibWR -> wrGrid, SpecsynLibWD -> wdGrid, SpecsynLibNoWind -> normalGrid) – so a GridType with no library of that kind in the chain at all is left at quiet_NaN(). Likewise scans every chained SpecsynLibNoWind or SpecsynLibWD library’s own log(g) grid (via logg()) for loggMin_[normalGrid]/loggMax_[normalGrid] and loggMin_[wdGrid]/loggMax_[wdGrid] respectively – GridType::wrGrid is never touched here, since Wolf-Rayet atmospheres aren’t parameterized by logg at all. If tClamp is false, every one of these four arrays stays entirely quiet_NaN(), which – per classifyGridType’s own comment – means a star can never be classified GridType::wdGrid at all (only GridType::wrGrid, decided independently of these arrays, or GridType::normalGrid). These arrays exist purely to drive that classification decision now (see spec()’s own comment): unlike before this class’s refactor to per-type chains and specForce(), spec() itself no longer clamps a star’s properties into any of these ranges before dispatching it.
wnlTeffRanges_ is computed separately, and unconditionally (regardless of tClamp): every chained SpecsynLibWR library’s own type()/logTeff() range is scanned into it, and it is then handed to every one of those same libraries via their own setWNLTeffRanges() – see wnlTeffRanges_’s own comment for why this, unlike the tClamp-gated arrays above, is not optional.
- Parameters:
spectraName – The names of the spectral models to chain together. Each is sorted, in the order given, into one of three per-GridType chains (see
fehMin – Minimum [Fe/H] value
fehMax – Maximum [Fe/H] value
afe – Value of [alpha/Fe]
cfe – Value of [C/Fe]
microTurb – Microturbulent velocity, in km/s, one entry per entry of spectraName (different libraries – e.g. an OB-star library versus one covering cooler, lower-mass stars – commonly assume different microturbulent velocities). An empty vector (the default) uses each library’s own micro_default value from the registry instead (see SpecsynLib’s constructor), rather than forcing every library in the chain to share one value.
r – Spectral resolution
registryName – Name of the spectral library registry file
wlMin – Minimum wavelength of the output grid, in Angstrom; if 0 (the default), used together with wlMax and nWl below (see
wlMax – Maximum wavelength of the output grid, in Angstrom; see wlMin
nWl – Number of points in the output grid; if 0 (the default), used as a flag to fall back on each library’s own native wavelength grid – see
tClamp – Whether classifyGridType() may ever route a star to GridType::wdGrid at all; defaults to true. See
controls – Simulation controls; forwarded unchanged to Specsyn’s own constructor – see its comment. Has no default of its own (unlike every parameter above): a reference bound to a temporary default-constructed SimControls would dangle the moment this constructor returned, since this class stores it live rather than copying out of it.
- Throws:
std::runtime_error – if spectraName is empty, or if microTurb is non-empty and its size does not match spectraName’s
-
virtual auto spec(const StarData &props, double feh) const -> std::vector<double> override
Compute a star’s spectrum from whichever chain matches its own GridType.
First decides which GridType this star belongs to – see classifyGridType’s own comment for the exact rule (in short: WR if getWRType says so; else WD if the star lies above the normal grids’ own coverage on log(Teff) or log(g), but the WD grids’ own floor still plausibly covers it on both; else normal) – and selects that GridType’s own chain (wrLibs_, wdLibs_, or normalLibs_).
If (and only if) the star is normal-grid, its own log(Teff) is clamped up to logTeffMin_[normalGrid] – the coolest floor among every chained normal library – if it falls below that floor, since normal libraries have no rescue of their own for an out-of-range log(Teff) the way they do for log(g) (see SpecsynLibNoWind::specForce()). Only the lower bound is clamped: a normal-grid star exceeding the upper log(Teff) bound should already have been routed to wdGrid or wrGrid above, so if one still reaches here, that is a genuine gap worth throwing on rather than silently clamping away.
Then tries each library in that chain, in priority order, stopping at and returning the first non-empty spec() result – in two passes. The first pass queries every library at the star’s own true feh and log(g), unclamped: chain order encodes each library’s physical reliability, not just its [Fe/H]/log(g) reach (e.g. TLUSTY’s NLTE hot-star models are preferred over CK04’s older, coarser physics even where both cover the same star), so a library later in the chain that happens to reach the true feh/log(g) natively must not preempt an earlier, more reliable library that also reaches it. Only if every library rejects the true feh/log(g) does a second pass retry each library with feh and log(g) each clamped to that one library’s own real coverage (fehMin_[type][i]/fehMax_[type][i] and loggLibMin_[type][i]/loggLibMax_[type][i], see clampFehForLibrary()) immediately before each individual spec() call, not once for the whole chain: a library whose own coverage doesn’t reach the true feh/log(g) at all should still be given its best (clamped) shot rather than being skipped outright, exactly mirroring how SpecsynLibNoWind:: specForce() itself moves log(g) to the nearest populated grid value rather than giving up – and, per this same priority ordering, a clamped result from an earlier, more-reliable library is preferred over an unclamped one from a later, less-reliable library, on the (unverified per star) assumption that a modest clamp costs less accuracy than falling back to worse atmosphere physics; how far the clamp reaches is not weighed against that tradeoff. No atmosphere-grid library actually has uniform coverage across its own [Fe/H] or log(g) range in practice (e.g. MARCS’s afe=0.0 models stop well short of its afe=0.4 models’ own [Fe/H] reach; a hot O/B star metal-poor enough to fall outside TLUSTY_O/TLUSTY_B’s own narrower [Fe/H] range needs the same kind of rescue BOSZ/CK04/MARCS already need for other reasons; a very metal-poor massive MIST star compact enough to exceed TLUSTY_O’s/CK04’s own log(g) ceiling needs the same kind of rescue on that axis instead) – see warnIfFeHClamped()’s own comment for how the [Fe/H] side of this is surfaced up front rather than left silent (log(g) has no equivalent up-front warning, since – unlike [Fe/H] – there is no single requested log(g) range fixed at construction time to check a library’s own coverage against: log(g) is only known per star, at spec() call time). log(g) is clamped by rescaling queryProps’ own mass, not by passing a separately clamped value – see propsWithClampedLogg()’s own comment for why. If every library in the chain still returns empty even after the clamped pass, calls specForce() (see Specsyn’s own comment) on the last one, clamped to its own coverage the same way, which either returns a genuine spectrum (forcing a match, e.g. by moving log(g) to the nearest populated grid value – see SpecsynLibNoWind::specForce()/SpecsynLibWDspecForce()) or throws for a reason other than [Fe/H]/log(g) alone (e.g. log(Teff) genuinely outside every chained library’s own grid).
- Parameters:
props – Stellar properties, as produced by evaluating the Interpolator1D returned by Tracks2D::getIsochrone at this star’s mass
feh – [Fe/H] value of the star; needed because it is not carried by props itself, and not necessarily within any single chained library’s own real coverage – see this function’s own
- Throws:
std::runtime_error – if this star’s own GridType has no chained library at all
std::runtime_error – if the spectrum about to be returned contains any non-finite value, rather than returning it – a NaN for even one star would otherwise silently contaminate the spectrum (and every photometric value derived from it) of whatever population it belongs to
- Returns:
The star’s spectrum, evaluated on the wavelength grid returned by wl(), in units of erg/s/Angstrom – never a size-0 vector
-
virtual auto specForIntegration(const StarData &props, double feh) const -> std::vector<double> override
Compute a star’s spectrum for Specsyn::continuousSpecIntegrand()’s own benefit.
Overrides Specsyn::specForIntegration() only to keep the override itself documented and present (the base class’s own default already just calls spec() unchanged – see its own comment) – now that spec() itself clamps feh per-library rather than leaving normal-/WD-grid stars deliberately un-rescued (see spec()’s own comment for why that distinction no longer applies), there is nothing left for this override to do differently, so it simply is spec().
-
inline auto wnlTeffRanges() const -> const std::array<std::pair<double, double>, 3>&
Get the log(Teff) range spanned by each chained WNL bucket.
Exposed so that code outside spectral synthesis (e.g. feedback::Winds::vWind()) can classify Wolf-Rayet stars exactly as this library itself does.
- Returns:
A const reference to wnlTeffRanges_ – see its own comment; suitable for passing directly to SpecsynLibWR::getWRType’s own wnlTeffRanges parameter
-
inline auto normalLogTeffMax() const -> double
Get the highest log(Teff) covered by any chained ordinary-star library.
Exposed for the same reason as wnlTeffRanges().
- Returns:
normalLogTeffMax_ – see its own comment; suitable for passing directly to SpecsynLibWR::getWRType’s own normalLogTeffMax parameter
Public Static Functions
-
static auto makeCommonWlGrid(const std::vector<std::vector<double>> &wlGrids) -> std::vector<double>
Build a common wavelength grid spanning several libraries’ own grids.
Splits the full wavelength range into windows delineated by every grid’s minimum and maximum wavelength – e.g. grids covering [200, 2000] and [1500, 3000] Angstrom split the range into windows [200, 1500], [1500, 2000], and [2000, 3000]. Within each window, restricts to grids whose own range fully contains it, and adopts whichever of those has the most points there (ties favor the earlier grid in wlGrids); this “fully contains” restriction matters because a grid whose own range happens to end (or start) exactly at one edge of a window still has that one boundary point technically inside it, which could otherwise spuriously tie against (or even beat) another grid that actually spans the window. If no grid fully contains a window, falls back to whichever grid simply has the most points physically present there, so a grid’s own trailing edge point is still kept when nothing else reaches anywhere near it; a window with no points from any grid at all (a genuine coverage gap) contributes nothing to the result, rather than inventing samples there. The result is these per-window selections concatenated in order, so it is itself sorted and spans the union of every input grid’s range wherever that union has no gaps.
This is a general-purpose grid-merging utility, not tied to SpecsynLibChained’s own constructor use of it (which passes the wl() of each of its chained libraries), so it takes wavelength grids directly rather than Specsyn/SpecsynLib objects.
- Parameters:
wlGrids – One wavelength grid per library, each sorted ascending and non-empty
- Throws:
std::runtime_error – if wlGrids, or any grid in it, is empty
- Returns:
A single sorted, deduplicated wavelength grid spanning the full range covered by wlGrids
-
SpecsynLibChained(const std::vector<std::string> &spectraName, double fehMin, double fehMax, double afe, double cfe, const std::vector<double> µTurb, double r, const std::string ®istryName, double wlMin, double wlMax, std::size_t nWl, bool tClamp, const io::SimControls &controls)