"""
SheapModelBuilder
==============
Builds spectral fitting regions from wavelength bounds and YAML templates.
It assembles narrow/broad/outflow/wind/BAL/FeII components, adds continuum
(and optional Balmer continua), and generates parameter‑tying rules suitable
for downstream fitting.
Main features
-------------
- Load line definitions from YAML repositories within a wavelength window.
- Instantiate `SpectralLine` objects for narrow/broad/outflow/wind/BAL/FeII.
- Add continuum components (power law, linear; Balmer/Balmer high order optional).
- Group lines into SPAF composites and apply known/amplitude ties.
- Produce a `SheapModel` and a fitting routine configuration.
Parameters
----------
xmin : float
Lower wavelength bound of the region (Å).
xmax : float
Upper wavelength bound of the region (Å).
n_narrow : int, optional
Number of narrow components per line (default: 1).
n_broad : int, optional
Number of broad components per line (default: 1).
line_repository_path : list[str | pathlib.Path], optional
Paths to YAML files defining line templates. If not provided, loads all
templates in ``SuportData/LineRepository``.
fe_mode : {"template", "model", "none"}, optional
How to include FeII emission (default: "template").
continuum_profile : str, optional
Continuum profile name (keys of ``PROFILE_CONTINUUM_FUNC_MAP``; default: "powerlaw").
group_method : bool, optional
If True, group lines and apply default ties automatically (default: True).
add_outflow : bool, optional
Include outflow components for selected narrow lines (default: False).
add_winds : bool, optional
Include wind components for selected broad lines (default: False).
add_balmer_continuum : bool, optional
Include Balmer continuum component (default: False).
add_balmerhighorder_continuum : bool, optional
Include Balmer high‑order continuum template (default: False).
add_uncommon_narrow : bool, optional
Include lines marked as uncommon (default: False).
add_host_miles : bool | dict, optional
Include a host‑galaxy template from MILES (``True`` for defaults or a dict of kwargs).
tied_narrow_to : str | dict, optional
Main line (or per‑component map) to which narrow lines are tied.
tied_broad_to : str | dict, optional
Main line (or per‑component map) to which broad lines are tied.
n_max_component_outflow : int, optional
Max outflow components per line (default: 1).
n_max_component_winds : int, optional
Max wind components per line (default: 1).
n_max_component_bal : int, optional
Max BAL components per line (default: 1).
verbose : bool, optional
Print informational messages (default: True).
Attributes
----------
lines_available : dict[str, list[dict]]
Loaded line definitions keyed by YAML stem.
pseudo_region_available : list[str]
Available pseudo‑region keys found in YAML.
sheapmodel : SheapModel
Final container with all built `SpectralLine` objects.
known_tied_relations : list[tuple]
Default amplitude/center ties used during grouping when enabled.
Examples
--------
>>> cb = SheapModelBuilder(6500, 6600, n_narrow=2, n_broad=1, add_outflow=True)
>>> cb.make_region()
>>> config = cb._make_fitting_routine(
... list_num_steps=[2000, 2000],
... list_learning_rate=[1e-1, 1e-2]
... )
Notes
-----
- If the wavelength span is shorter than ``LINEAR_RANGE_THRESHOLD``, the
continuum is forced to linear for stability.
- ``group_method=True`` collapses lines into SPAF composites per region/element
and applies known ties (e.g., [O III], [N II]) automatically.
- FeII “template” mode inserts broad templates spanning the requested range;
“model” loads individual FeII lines; “none” skips FeII.
"""
from __future__ import annotations
__author__ = 'felavila'
__all__ = ["SheapModelBuilder",]
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Union
import warnings
import numpy as np
import yaml
from sheap.Core import SpectralLine,SheapModel
from sheap.SheapModelBuilder.Utils import fe_ties, _maketies, group_lines # asistant material
from sheap.Profiles.Profiles import PROFILE_CONTINUUM_FUNC_MAP
from sheap.Profiles.profiles_templates import make_host_function #?
#TODO ADD the rutines of gaussians and tied methods in general.
#TODO make lines a only one big file
# Balmer continuum, Balmer High order emission lines
# 3646.0 limit for balmer continuum after this we can move to another stuff
# ADD NLR AS KIND LINE SEARCH FOR NLR PRONT IN THE SPECTRA
[docs]
class SheapModelBuilder:
"""
Builds spectral fitting regions given wavelength bounds and YAML templates,
including narrow, broad, outflow, and FeII components, plus parameter tying.
Parameters
----------
xmin : float
Lower wavelength bound of the region (Å).
xmax : float
Upper wavelength bound of the region (Å).
n_narrow : int, optional
Number of narrow components per line, by default 1.
n_broad : int, optional
Number of broad components per line, by default 1.
line_repository_path : list of str or Path, optional
Paths to YAML files defining line templates. Defaults to all in `LineRepository/`.
fe_mode : {'template', 'model', 'none'}, optional
Mode for FeII components, by default "template".
continuum_profile : str, optional
Continuum profile name (must be in PROFILE_CONTINUUM_FUNC_MAP), by default "powerlaw".
group_method : bool, optional
Whether to apply automatic grouping and tying of parameters, by default True.
add_outflow : bool, optional
Include outflow components for narrow lines, by default False.
add_winds : bool, optional
Include wind components for broad lines, by default False.
add_balmer_continuum : bool, optional
Include Balmer continuum component, by default False.
add_balmerhighorder_continuum : bool, optional
Include Balmer high order continuum component, by default False.
add_uncommon_narrow : bool, optional
Include uncommon narrow lines, by default False.
add_host_miles : bool or dict, optional
Include host‐galaxy template from MILES; if dict, passes its keys to the builder.
verbose : bool, optional
Print informational messages during building, by default True.
Attributes
----------
lines_available : dict[str, list of dict]
Loaded line definitions from YAML.
pseudo_region_available : list[str]
Keys of available pseudo‑regions from YAML.
sheapmodel : SheapModel
Container of all SpectralLine objects after building.
tied_relations : list
Parameter‐tying specifications used in fitting routine.
Examples
--------
>>> rb = SheapModelBuilder(6500, 6600, n_narrow=2, n_broad=1, add_outflow=True)
>>> rb.make_region()
>>> routine = rb._make_fitting_routine(list_num_steps=[2000,2000], list_learning_rate=[1e-1,1e-2])
"""
lines_prone_outflow = ["CII]","[NeV]a","[NeV]b","OIIIc","OIIIb","NeIIIa","OIIb","OIIa","[NeIV]","[OII]","[NeIII]"]#,"NIIb","NIIa","SIIb","SIIa",]
lines_prone_winds = ["Lyalpha","CIV","AlIII","MgII","Halpha","Hbeta"]#,"HeIe","HeIk","HeIId"] Lyα
lines_prone_bal = ["CIV","AlIII","MgII","NV","SiIV","OIV]"," OVIa"," OVIb"]#,"HeIe","HeIk","HeIId"]
available_fe_modes = ["template","model","none"] # none is like No fe
available_continuum_profiles = list(PROFILE_CONTINUUM_FUNC_MAP.keys()) + ["none"]
LINEAR_RANGE_THRESHOLD = 1000
known_tied_relations: List[Tuple[Tuple[str, ...], List[str]]] = [(('OIIIb', 'OIIIc'),['amplitude_OIIIb_component_narrow', 'amplitude_OIIIc_component_narrow', '*0.3'],),
(('NIIa', 'NIIb'),['amplitude_NIIa_component_narrow', 'amplitude_NIIb_component_narrow', '*0.3'],),
(('NIIa', 'NIIb'), ['center_NIIa_component_narrow', 'center_NIIb_component_narrow']),
(('OIIIb', 'OIIIc'),['center_OIIIb_component_narrow', 'center_OIIIc_component_narrow'],),]
def __init__(
self,
xmin: float,
xmax: float,
n_narrow: int = 1,
n_broad: int = 1,
line_repository_path: Optional[List[Union[str, Path]]] = None,
fe_mode = "template",
continuum_profile = "powerlaw",
group_method = True,
add_outflow = False,
add_winds = False,
add_balmer_continuum = False,
add_uncommon_narrow = False,
add_BAL = False,
add_balmerhighorder_continuum = False,
add_host_miles: Optional[Union[Dict,bool]] = None,
tied_narrow_to: Optional[Union[str, Dict[int, Dict[str, int]]]] = None,
tied_broad_to: Optional[Union[str, Dict[int, Dict[str, int]]]] = None,
n_max_component_outflow = 1,
n_max_component_winds = 1,
n_max_component_bal = 1,
#fe_regions=['fe_uv', "feii_IZw1", "feii_forbidden", "feii_coronal"],
#fe_tied_params=('center', 'fwhm'),
#verbose=True,
**kwargs) -> None:
"""
Initialize the SheapModelBuilder with region bounds and options.
Parameters
----------
xmin : float
Minimum wavelength (Å) for region.
xmax : float
Maximum wavelength (Å) for region.
n_narrow : int, optional
Number of narrow-line components per line.
n_broad : int, optional
Number of broad-line components per line.
line_repository_path : list of str or Path, optional
Filepaths to YAML templates for lines.
fe_mode : str, optional
FeII handling mode: 'template', 'model', or 'none'.
continuum_profile : str, optional
Continuum profile to use ('powerlaw', etc.).
group_method : bool, optional
Whether to group and tie parameters automatically.
add_outflow : bool, optional
Whether to include outflow components.
add_winds : bool, optional
Whether to include wind components.
add_balmer_continuum : bool, optional
Whether to include Balmer continuum.
add_uncommon_narrow : bool, optional
Whether to include uncommon narrow lines.
add_host_miles : bool or dict, optional
Host-galaxy template inclusion or options dict.
verbose : bool, optional
Print status messages.
"""
self.xmin = xmin
self.xmax = xmax
self.n_narrow = n_narrow
self.n_broad = n_broad
self.group_method = group_method
self.add_balmer_continuum = add_balmer_continuum
self.fe_mode = fe_mode.lower()
self.add_outflow = add_outflow
self.add_winds = add_winds
self.add_uncommon_narrow = add_uncommon_narrow
self.add_balmerhighorder_continuum = add_balmerhighorder_continuum
self.verbose = kwargs.get("verbose",False)
self.add_host_miles = add_host_miles
self.tied_broad_to = tied_broad_to
self.tied_narrow_to = tied_narrow_to
self.add_BAL = add_BAL
self.n_max_component_outflow = n_max_component_outflow
self.n_max_component_winds = n_max_component_winds
self.n_max_component_bal = n_max_component_bal
if self.fe_mode not in self.available_fe_modes:
print(f"fe_mode: {self.fe_mode} not recognized moving to template, the current available are {self.available_fe_modes}")
self.fe_mode = "template"
###
if isinstance(continuum_profile,str):
continuum_profile = {"name":continuum_profile.lower(),"keywords":{"delta0":5500}}
self.continuum_profile = continuum_profile
if self.continuum_profile.get("name") not in self.available_continuum_profiles:
print(f"continuum_profile: {self.continuum_profile.get("name")} not recognized moving to powerlaw, the current available are {self.available_continuum_profiles}")
#print(self.continuum_profile)
self.continuum_profile = {"name": "powerlaw", "keywords": {"delta0": 5500}}
if not line_repository_path:
TEMPLATES_PATH = Path(__file__).resolve().parent.parent / "SuportData" / "LineRepository"
self.line_repository_path = list(TEMPLATES_PATH.glob("*.yaml"))
self.lines_available: Dict[str, Any] = {}
self._load_lines(self.line_repository_path) #this should be always here?
self.make_region()
[docs]
def make_region(
self,
xmin: Optional[float] = None,
xmax: Optional[float] = None,
n_broad: Optional[int] = None,
n_narrow: Optional[int] = None,
fe_mode: Optional[str] = None,
continuum_profile: Optional[str] = None,
group_method: Optional[bool] = None,
add_outflow= None,
add_winds = None,
add_balmer_continuum = None,
add_uncommon_narrow = None,
add_host_miles = None,
tied_broad_to= None,
tied_narrow_to = None,
add_BAL = None,
add_balmerhighorder_continuum = None):
"""
Build a `SheapModel` of `SpectralLine` objects based on settings.
Parameters
----------
xmin : float, optional
Override for lower wavelength bound.
xmax : float, optional
Override for upper wavelength bound.
n_broad : int, optional
Override for broad-line count.
n_narrow : int, optional
Override for narrow-line count.
fe_mode : str, optional
Override for FeII mode.
continuum_profile : str, optional
Override for continuum profile.
group_method : bool, optional
Override for grouping behavior.
add_outflow : bool, optional
Override for outflow inclusion.
add_winds : bool, optional
Override for wind inclusion.
add_balmer_continuum : bool, optional
Override for Balmer continuum.
add_uncommon_narrow : bool, optional
Override for uncommon narrow-line inclusion.
add_host_miles : bool or dict, optional
Override for host-galaxy options.
"""
def get(val, fallback):
return val if val is not None else fallback
xmin = get(xmin, self.xmin)
xmax = get(xmax, self.xmax)
n_broad = get(n_broad, self.n_broad)
n_narrow = get(n_narrow, self.n_narrow)
fe_mode = get(fe_mode, self.fe_mode).lower()
add_outflow = get(add_outflow, self.add_outflow)
add_balmer_continuum = get(add_balmer_continuum, self.add_balmer_continuum)
add_winds = get(add_winds, self.add_winds)
add_uncommon_narrow = get(add_uncommon_narrow,self.add_uncommon_narrow)
add_host_miles = get(add_host_miles,self.add_host_miles)
add_BAL = get(add_BAL,self.add_BAL)
add_balmerhighorder_continuum = get(add_balmerhighorder_continuum,self.add_balmerhighorder_continuum)
# continuum_profile => only can be a dict?
continuum_profile = get(continuum_profile, self.continuum_profile)#.lower()
#
tied_broad_to = get(tied_broad_to,self.tied_broad_to)
tied_narrow_to = get(tied_narrow_to,self.tied_narrow_to)
if add_BAL:
warnings.warn("The addition of BALs to the fit is still in development not well tested yet.")
if fe_mode not in self.available_fe_modes:
print(fe_mode)
print(f"fe_mode: {fe_mode} not recognized moving to template, the current available are {self.available_fe_modes}")
fe_mode = "template"
if continuum_profile.get("name") not in self.available_continuum_profiles:
print(f"continuum_profile: {continuum_profile.get("name")} not recognized moving to powerlaw, the current available are {self.available_continuum_profiles}")
#print(self.continuum_profile)
self.continuum_profile = {"name": "powerlaw"}
self.group_method = get(group_method,self.group_method)
self.region_list = [] #place holder name
for pseudo_region_name,list_dict in self.lines_available.items():
comps = []
for raw_line in list_dict:
center = float(raw_line.get('center', -np.inf))
if not (xmin <= center <= xmax):
continue
base = SpectralLine(**raw_line)
if pseudo_region_name == "broad_and_narrow": #search of name
comps = self._handle_broad_and_narrow_lines(base, n_narrow, n_broad,add_winds=add_winds,add_BAL=add_BAL,add_outflow=add_outflow)
elif pseudo_region_name == "narrows" and (n_narrow>0 or add_outflow):
#print("jeje")
comps = self._handle_narrow_line(base, n_narrow,add_outflow=add_outflow,add_uncommon_narrow=add_uncommon_narrow)
elif pseudo_region_name == "broads" and n_broad>0:
comps = self._handle_broad_line(base, n_broad,add_winds=add_winds,add_BAL=add_BAL)
self.region_list.extend(comps)
if add_host_miles:
self._handle_host(add_host_miles,xmin,xmax)
#print(fe_mode)
self.region_list.extend(self._handle_fe(fe_mode,xmin,xmax))
self.region_list.extend(self._continuum_handle(continuum_profile,xmin,xmax,add_balmer_continuum=add_balmer_continuum,add_balmerhighorder_continuum = add_balmerhighorder_continuum))#here we already are able to create the sheapmodel
#sheapmodel
self.sheapmodel = SheapModel(self.region_list)
self._ties = []
self._known_ties = []
self._feties = []
if self.group_method:
self.sheapmodel = self._apply_group_method(self.sheapmodel,fe_mode,self.known_tied_relations)
else:
#todo add the tied_broad_to and narrow_to in cases in where is best use a line selected for the user
#print(self.known_tied_relations)
self._ties,self._known_ties =_maketies(self.sheapmodel,tied_narrow_to = tied_narrow_to, tied_broad_to = tied_broad_to,known_tied_relations=self.known_tied_relations)
#self.tied_relations.extend([*_ties,*_known_ties])
#self._ties = []
if fe_mode not in ["none","template"]:
routine_fe_tied = {"by":"subregion","tied_params": ('center', 'fwhm')}
self._feties = fe_ties(self.sheapmodel.group_by("region").get("fe").lines, routine_fe_tied)
#self.tied_relations.extend(fe_ties(self.sheapmodel.group_by("region").get("fe").lines, routine_fe_tied))
del self.region_list
def _handle_broad_and_narrow_lines(
self, entry: SpectralLine, n_narrow: int, n_broad: int, add_winds=False,add_BAL = False ,add_outflow=False,) -> List[SpectralLine]:
"""
Create narrow, broad, and optional wind components for a single line.
Parameters
----------
entry : SpectralLine
Base line definition.
n_narrow : int
Number of narrow components.
n_broad : int
Number of broad components.
add_winds : bool, optional
Whether to append a wind component to first broad line.
Returns
-------
list of SpectralLine
Generated line components.
"""
comps: List[SpectralLine] = []
total = n_narrow + n_broad
for idx in range(total):
region = 'narrow' if idx < n_narrow else 'broad'
comp_num = idx + 1 if region == 'narrow' else idx - n_narrow + 1
amp = 1.0 #if comp_num == 1 else 1.0/comp_num
new = SpectralLine(
center=entry.center,
line_name=entry.line_name,
region=region,
component=comp_num,
amplitude=amp,
element=entry.element,
)
comps.append(new)
#self.n_max_component_outflow
if add_outflow and comp_num <= self.n_max_component_outflow and new.line_name in self.lines_prone_outflow:
out = SpectralLine(
center= entry.center,
line_name=entry.line_name,
region ='outflow',
component = comp_num,
amplitude=1.0,
element = entry.element,
rarity = entry.rarity)
comps.append(out)
elif add_winds and comp_num <= self.n_max_component_winds and new.line_name in self.lines_prone_winds:
out = SpectralLine(
center= entry.center,
line_name=entry.line_name,
region ='winds',
component = comp_num,
amplitude=1.0,
element = entry.element,
)
comps.append(out)
elif add_BAL and comp_num <= self.n_max_component_bal and new.line_name in self.lines_prone_bal:
out = SpectralLine(
center= entry.center,
line_name=entry.line_name,
region ='bal',
component = comp_num,
amplitude=1.0,
element = entry.element,
)
comps.append(out)
return comps
def _handle_narrow_line(
self, entry: SpectralLine, n_narrow: int, add_outflow: bool = False, add_uncommon_narrow = False) -> List[SpectralLine]:
"""
Create narrow and optional outflow components for a single line.
Parameters
----------
entry : SpectralLine
Base line definition.
n_narrow : int
Number of narrow components.
add_outflow : bool, optional
Include an outflow component for the first narrow line.
add_uncommon_narrow : bool, optional
Include lines marked as 'uncommon'.
Returns
-------
list of SpectralLine
Generated narrow (and outflow) components.
"""
comps: List[SpectralLine] = []
for idx in range(n_narrow):
amp = 1.0
if entry.rarity=="uncommon" and not add_uncommon_narrow:
continue
comp_num = idx + 1
new = SpectralLine(
center=entry.center,
line_name=entry.line_name,
region ='narrow',
component = comp_num,
amplitude = amp,
element = entry.element,
rarity = entry.rarity)
comps.append(new)
#->ok.
if add_outflow and entry.line_name in self.lines_prone_outflow:
out = SpectralLine(
center= entry.center,
line_name=entry.line_name,
region ='outflow',
component = 1,
amplitude=1.0,
element = entry.element,
rarity = entry.rarity)
comps.append(out)
return comps
def _handle_broad_line(self, entry: SpectralLine, n_broad: int,add_winds=False,add_BAL=False) -> List[SpectralLine]:
"""
Create broad and optional wind components for a single line.
Notes
-----
Only for the first broad component is possible add the extra-broad lines.
Parameters
----------
entry : SpectralLine
Base line definition.
n_broad : int
Number of broad components.
add_winds : bool, optional
Include a wind component for the first broad line.
Returns
-------
list of SpectralLine
Generated broad (and wind) components.
"""
#extra broad?
#return comps
#print(n_broad)
comps: List[SpectralLine] = []
for idx in range(n_broad):
#if idx>0:
# continue
amp = 1 #if idx == 0 else 0.5
comp_num = idx + 1
new = SpectralLine(
center=entry.center,
line_name=entry.line_name,
region='broad',
component=comp_num,
amplitude=amp,
element=entry.element,
)
comps.append(new)
if add_winds and comp_num <= self.n_max_component_winds and self.lines_prone_winds:
out = SpectralLine(
center= entry.center,
line_name=entry.line_name,
region ='winds',
component = comp_num,
amplitude=1.0,
element = entry.element,
)
comps.append(out)
elif add_BAL and comp_num <= self.n_max_component_bal and new.line_name in self.lines_prone_bal:
out = SpectralLine(
center= entry.center,
line_name=entry.line_name,
region ='bal',
component = comp_num,
amplitude=1.0,
element = entry.element,
)
comps.append(out)
return comps
def _handle_fe(self,fe_mode,xmin,xmax):
"""
Generate FeII components based on selected mode and wavelength range.
Parameters
----------
fe_mode : {'template', 'model', 'none'}
FeII handling mode.
xmin : float
Lower bound of region.
xmax : float
Upper bound of region.
Returns
-------
list of SpectralLine
FeII line components (empty if mode 'none').
Notes
-----
- 'template' adds broad template components if range is sufficient.
- 'model' loads individual FeII lines from YAML.
"""
fe_comps = []
#print(fe_comps)
if fe_mode == "none":
#print("here")
return fe_comps
elif fe_mode == "template":
if self.verbose:
print("Added fe template")
fe_comps.extend([SpectralLine(line_name="feuvop",region="fe",component=1,profile="template",template_info = {"name":"feuvop","x_min":xmin,"x_max":xmax})])
# t_c = 0
# if max(0, min(xmax, 7484) - max(xmin, 3686)) >= 1000:
# if self.verbose:
# print("added OP template")
# fe_comps.extend(
# [SpectralLine(line_name="feop",region="fe",component=1,profile="fetemplate",template_info = {"name":"feop"})])
# t_c += 1
# if max(0, min(xmax, 3500) - max(xmin, 1200)) >= 500:
# #maybe it is a good time to r
# if self.verbose:
# print("added UV template")
# fe_comps.extend([SpectralLine(line_name="feuv",region="fe",component=1,profile="fetemplate",template_info = {"name":"feuv"})])
# t_c += 1
# if t_c == 0:
# print("The covered range is not valid for template use. Switching to model mode. Work in progress, if no Fe wanted put fe_mode = none.")#this have to be a warning
# fe_mode = "model"
elif fe_mode == "model":
if self.verbose:
print("Added model fe")
for pseudo_region_name,list_dict in self.lines_available.items():
for raw_line in list_dict:
center = float(raw_line.get('center', -np.inf))
if not (xmin <= center <= xmax) or pseudo_region_name not in ('feii_uv',"feii_model"):
continue
base = SpectralLine(**raw_line)
base.subregion = pseudo_region_name
base.component = 1
fe_comps.extend([base])
#print(fe_comps)
return fe_comps
def _continuum_handle(self,continuum_profile,xmin,xmax,add_balmer_continuum=False,add_balmerhighorder_continuum=False):
"""
Create continuum components: linear, powerlaw, or Balmer.
Parameters
----------
continuum_profile : str
Continuum profile to use.
xmin : float
Lower wavelength bound.
xmax : float
Upper wavelength bound.
add_balmer_continuum : bool, optional
Include a Balmer continuum component if True.
Returns
-------
list of SpectralLine
Continuum components.
"""
continuum_comps = []
if add_balmer_continuum:
if self.verbose:
print("added balmer continuum")
continuum_comps.append(SpectralLine(line_name='balmercontinuum',region='balmer',component=0,profile='balmercontinuum'))
if add_balmerhighorder_continuum:
if self.verbose:
print("added balmer high order continuum")
continuum_comps.append(SpectralLine(line_name='balmerhighorder',region='balmer',component=0,profile='template'
,template_info = {"name":"BalHiOrd","x_min":xmin,"x_max":xmax}))
if continuum_profile != "none":
continuum_comps.append(SpectralLine(line_name=continuum_profile.get("name"),region='continuum',component=0,profile=continuum_profile.get("name"),template_info=continuum_profile))
return continuum_comps
def _apply_group_method(self,sheapmodel,fe_mode,known_tied_relations):
"""
Group lines by region, apply known ties, and return a new SheapModel.
-This function in particular could be useful to run it outside.
Parameters
----------
sheapmodel : SheapModel
Ungrouped region object.
fe_mode : str
FeII mode for grouping logic.
known_tied_relations : list of tuple
Predefined tie relations for line ratios and centers.
Returns
-------
SheapModel
Grouped and tied region.
"""
dict_regions = sheapmodel.group_by("region")
new_region_list = []
for key,values in dict_regions.items():
if key in ["continuum","host","balmer"]:
new_region_list.extend(values.lines)
elif key == "fe":
#here much more can be done
if fe_mode=="model":
new_region_list.extend(group_lines(values.lines,key,mode="element",profile="SPAF"))
else:
new_region_list.extend(values.lines)
elif key in ["outflow","winds"]:
new_region_list.extend(group_lines(values.lines,key,mode="element",known_tied_relations=known_tied_relations,profile="SPAF"))
else:
new_region_list.extend(group_lines(values.lines,key,mode="region",known_tied_relations=known_tied_relations,profile="SPAF"))
return SheapModel(new_region_list)
def _handle_host(self,add_host_miles,xmin,xmax):
"""
Add a host-galaxy template component using MILES SSP models.
Parameters
----------
add_host_miles : dict or bool
Configuration for host model (kwargs for `make_host_function` or True for defaults).
xmin : float
Lower wavelength bound.
xmax : float
Upper wavelength bound.
Side Effects
------------
Appends a `SpectralLine` of region 'host' to `self.region_list`.
"""
pos_defaults = make_host_function.__defaults__ or ()
#kw_defaults = make_host_function.__kwdefaults__ or {}
argcount = make_host_function.__code__.co_argcount
all_args = make_host_function.__code__.co_varnames[:argcount]
names_for_pos = all_args[-len(pos_defaults):] if pos_defaults else []
defaults = {name: val for name, val in zip(names_for_pos, pos_defaults)}
defaults["xmax"] = xmax
defaults["xmin"] = xmin
defaults["verbose"] = False
if isinstance(add_host_miles,bool):
_host_model = make_host_function(**defaults)
elif isinstance(add_host_miles,Dict):
add_host_miles.update({"xmax":xmax,"xmin":xmin})
_host_model = make_host_function(**add_host_miles)
else:
Warning("Not accepted type of add_host_moles")
return self.region_list
line = SpectralLine(line_name="host",region="host",component=1,template_info=_host_model["host_info"],profile="hostmiles")
self.region_list.extend([line])
def _make_fitting_routine(self,list_num_steps = [1000],list_learning_rate = [1e-2]):
"""
Assemble the fitting routine dictionary for `RegionFitting.from_builder`.
TODO improve the loop.
Parameters
----------
list_num_steps : list of int, optional
Number of optimization steps for each stage.
list_learning_rate : list of float, optional
Learning rates for each stage.
Returns
-------
dict
Dictionary with keys 'sheapmodel', 'outer_limits', 'inner_limits',
and 'fitting_routine' ready for passing to RegionFitting.
Raises
------
AssertionError
If lengths of `list_num_steps` and `list_learning_rate` differ.
"""
fitting_routine = {}
tied_relations = [self._known_ties]
if len(self._ties) > 0:
list_num_steps.extend([1000])
list_learning_rate.extend([1e-2])
tied_relations = [[*self._ties,*self._known_ties,*self._feties],[*self._known_ties]]
assert len(list_num_steps) == len(list_learning_rate), "len(list_num_steps) != len(list_learning_rate) "
if not self.group_method:
for i, steps in enumerate(list_num_steps):
if i>0:
fitting_routine[f"step{i+1}"] = {"tied": tied_relations[1],"non_optimize_in_axis": 4,"learning_rate": list_learning_rate[i],"num_steps": list_num_steps[i]}
else:
fitting_routine[f"step{i+1}"] = {"tied": tied_relations[i],"non_optimize_in_axis": 4,"learning_rate": list_learning_rate[i],"num_steps": list_num_steps[i]}
#fitting_routine[f"step{steps}"] = {"tied": [],"non_optimize_in_axis": 4,"learning_rate":list_learning_rate[i-1],"num_steps": list_num_steps[i-1]}
else:
for i, steps in enumerate(list_num_steps):
if i>0:
fitting_routine[f"step{i+1}"] = {"tied": [],"non_optimize_in_axis": 4,"learning_rate": list_learning_rate[i],"num_steps": list_num_steps[i]}
else:
fitting_routine[f"step{i+1}"] = {"tied": [],"non_optimize_in_axis": 4,"learning_rate": list_learning_rate[i],"num_steps": list_num_steps[i]}
return {"sheapmodel": self.sheapmodel,"outer_limits": [self.xmin, self.xmax], "inner_limits": [self.xmin + 50, self.xmax - 50],"fitting_routine":fitting_routine}
def _load_lines(self, paths: Optional[List[Union[str, Path]]]) -> None:
"""
Load YAML files defining spectral lines into `lines_available`.
Parameters
----------
paths : list of str or Path, optional
Paths to YAML templates.
Raises
------
ValueError
If no paths are provided.
FileNotFoundError
If any path does not point to an existing file.
KeyError
If YAML content is not a list of dicts per file.
"""
if not paths:
raise ValueError("No YAML paths provided for region templates.")
for p in paths:
path = Path(p)
if not path.is_file():
raise FileNotFoundError(f"Region YAML not found: {path}")
data = yaml.safe_load(path.read_text())
key = path.stem
if not isinstance(data, list) and not all(isinstance(item, dict) for item in data):
raise KeyError(f"Not all element in the YAML are list filled with dict: {path}")
self.lines_available[key] = data
self.pseudo_region_available = list(self.lines_available.keys())