diff --git a/src/besta/grid/grid.py b/src/besta/grid/grid.py index fbaa31c6..766e154c 100644 --- a/src/besta/grid/grid.py +++ b/src/besta/grid/grid.py @@ -35,13 +35,10 @@ from besta.postprocess import ( enclosed_fraction_map, - pit_from_discrete_posterior, pdf_stats, - photoz_metrics, weighted_quantiles, ) -from besta.utils import available_memory_bytes from besta.logging import get_logger os.environ.setdefault("OMP_NUM_THREADS", "1") diff --git a/src/besta/pipeline_modules/base_module.py b/src/besta/pipeline_modules/base_module.py index b589e1cb..5d2776c2 100644 --- a/src/besta/pipeline_modules/base_module.py +++ b/src/besta/pipeline_modules/base_module.py @@ -662,6 +662,9 @@ def prepare_observed_spectra( if not (instrumental_lsf == 0).all(): self.config["lsf"] = instrumental_lsf + if options.get_bool("use_features", default=True): + self.get_feature_weights(options) + self.config["weights"] *= self.config["feature_weights"] _log("Configuration done.") def prepare_galaxy(self, options): @@ -811,6 +814,7 @@ def prepare_losvd_kernel(self, options): _log("Configuration done") def get_feature_weights(self, options): + """TODO""" logger.info("Computing feature weights from input spectra") # Estimate the continuum continuum, continuum_err = spectrum.estimate_continuum( @@ -824,12 +828,10 @@ def get_feature_weights(self, options): self.config["continuum"] = continuum self.config["continuum_err"] = continuum_err # Favour features over/under continuum - w = (np.abs(self.config["flux"] - continuum) / continuum_err)**2 + weight_powlaw = options.get_double("feature_weight_powlaw", 2.0) + w = (np.abs(self.config["flux"] - continuum) / continuum_err)**weight_powlaw w = np.where(np.isfinite(w), w, 0.0) - w_sum = np.nansum(w) - if w_sum <= 0: - raise ValueError("Feature-based weights sum to zero; please check the input data or disable feature-based weighting.") - w /= w_sum + w /= w.max() self.config["feature_weights"] = w def measure_emission_lines(self, solution: DataBlock, **kwargs): @@ -1054,6 +1056,12 @@ def plot_solution(self, solution: DataBlock, figname=None, plot_lines=True): ax.set_yscale("symlog", linthresh=1.0) ax.set_xlabel("Wavelength (AA)") + twax = ax.twinx() + twax.fill_between( + np.array(self.config["wavelength"]), 0.0, weights, + color="lime", alpha=0.2, label="Weights") + twax.set_ylabel("Weight") + ax = axs[1, 1] ax.hist( chi2, diff --git a/src/besta/postprocess.py b/src/besta/postprocess.py index 6b5bfe6b..125b94da 100644 --- a/src/besta/postprocess.py +++ b/src/besta/postprocess.py @@ -350,24 +350,24 @@ def histogram_pdf_1d( # Convert probability per bin -> density with np.errstate(divide="ignore", invalid="ignore"): pdf = hist / dx - centers = 0.5 * (edges[:-1] + edges[1:]) # Ensure integrates to 1 (numerical) integral = np.nansum(pdf * dx) if integral > 0: pdf /= integral - return centers, pdf + return edges, pdf def kde_pdf_1d( x: np.ndarray, weights: np.ndarray, - grid: np.ndarray, + bins: np.ndarray, ) -> np.ndarray: """ KDE PDF on a provided grid; returns NaNs on failure. """ x = _as_float_array(x).ravel() w = _as_float_array(weights).ravel() - g = _as_float_array(grid).ravel() + edges = _as_float_array(bins).ravel() + g = 0.5 * (edges[:-1] + edges[1:]) mask = np.isfinite(x) & np.isfinite(w) x = x[mask] w = w[mask] @@ -450,6 +450,92 @@ def kde_or_hist_pdf_2d( Z /= integral return xc, yc, Z +def pit_from_pdf(x_edges, pdf, x_true): + """ + Compute PIT value for a true value given a PDF defined by edges and values. + + Parameters + ---------- + x_edges : array shape (N+1,) bin edges + pdf : array shape (N,) PDF values for each bin, normalized to integrate to 1 + x_true : scalar true value + + Returns + ------- + pit : scalar in [0,1] representing the cumulative probability up to ``x_true`` + """ + x_edges = _as_float_array(x_edges).ravel() + pdf = _as_float_array(pdf).ravel() + if x_edges.size != pdf.size + 1: + raise ValueError("x_edges must have one more element than pdf.") + if not np.isfinite(x_true): + raise ValueError("x_true must be finite.") + dx = np.diff(x_edges) + cdf = np.cumsum(pdf * dx) + if not np.isclose(cdf[-1], 1.0): + raise ValueError("pdf must be normalized to integrate to 1.") + + return np.interp(x_true, x_edges, np.r_[0.0, cdf]) + +def pdf_stats(edges: np.ndarray, pdf: np.ndarray, + quantiles=None, + find_multimodal: bool = False) -> dict: + """ + Compute summary stats from a discrete pdf over bin centers. + + Parameters + ---------- + edges : ndarray, shape (K,) + Bin edges. + pdf : ndarray, shape (K,) + PDF defined by the edges. + quantiles : tuple of float, optional + Quantiles to report (default 16, 50, 84 percent). + find_multimodal : bool, optional + If True, return rough modes by local-maximum search. + + Returns + ------- + stats : dict + Keys: mean, std, map, q, lo68, hi68, modes (optional). + """ + # Ensure normalization + norm = np.sum(pdf * np.diff(edges)) + pdf /= norm if norm > 0 else 1.0 + centers = 0.5 * (edges[:-1] + edges[1:]) + # Trapecium integrals + mean = np.sum(pdf * centers * np.diff(edges)) + var = np.sum(pdf * (centers - mean)**2 * np.diff(edges)) + std = var ** 0.5 + k_map = np.argmax(pdf * np.diff(edges)) + v_map = centers[k_map] + + cdf = np.cumsum(pdf * np.diff(edges)) + cdf = np.insert(cdf, 0, 0) + + if quantiles is not None: + qs = np.array(quantiles, float) + qvals = np.interp(qs, cdf, edges, left=edges[0], right=edges[-1]) + else: + qvals = None + + median = np.interp(0.5, cdf, edges, left=edges[0], right=edges[-1]) + lo68 = np.interp(0.16, cdf, edges, left=edges[0], right=edges[-1]) + hi68 = np.interp(0.84, cdf, edges, left=edges[0], right=edges[-1]) + + out = {"mean": mean, "std": std, "map": v_map, "q": qvals, + "median": median, "lo68": lo68, "hi68": hi68} + + if find_multimodal: + modes = [] + for i in range(1, len(pdf) - 1): + if pdf[i] > pdf[i - 1] and pdf[i] > pdf[i + 1]: + modes.append(centers[i]) + if not modes: + modes = [v_map] + out["modes"] = np.asarray(modes) + return out + # ----------------------------------------------------------------------------- # Autocorrelation # ----------------------------------------------------------------------------- @@ -863,6 +949,11 @@ def _tolist(a): } return out + @classmethod + def from_json(cls): + #TODO + pass + def write_json(self, path: str, *, overwrite: bool = True, indent: int = 2) -> str: """Write a JSON summary file.""" if (not overwrite) and os.path.exists(path): @@ -939,15 +1030,27 @@ def to_fits(self) -> fits.HDUList: hdus.append(fits.BinTableHDU(t_pct, name="PERCENTILES", header=pct_hdr)) - # PDF1D table: store grid/pdf/kde per parameter as separate columns + # PDF1D table: store edges/pdf/kde per parameter as separate columns t_pdf1 = Table() + t_pdf1_edges = Table() for name, d in self.pdf_1d.items(): - t_pdf1[f"{name}_x"] = _as_float_array(d["grid"]) - t_pdf1[f"{name}_pdf"] = _as_float_array(d["hist_pdf"]) - t_pdf1[f"{name}_kde"] = _as_float_array(d.get("kde_pdf", np.full_like(d["grid"], np.nan))) + edges = _as_float_array(d["edges"]) + centers = _as_float_array(d.get("grid", 0.5 * (edges[:-1] + edges[1:]))) + hist_pdf = _as_float_array(d["hist_pdf"]) + kde_pdf = _as_float_array(d.get("kde_pdf", np.full_like(hist_pdf, np.nan))) + + # FITS bin table columns must have consistent lengths across rows. + # Store centers/PDF/KDE in PDF1D (all length = n_bins). + t_pdf1[f"{name}_x"] = centers + t_pdf1[f"{name}_pdf"] = hist_pdf + t_pdf1[f"{name}_kde"] = kde_pdf + # Store raw histogram edges separately (length = n_bins + 1). + t_pdf1_edges[f"{name}_edges"] = edges if len(t_pdf1.colnames) > 0: hdus.append(fits.BinTableHDU(t_pdf1, name="PDF1D")) + if len(t_pdf1_edges.colnames) > 0: + hdus.append(fits.BinTableHDU(t_pdf1_edges, name="PDF1D_EDGES")) # PDF2D images for (n0, n1), d in self.pdf_2d.items(): @@ -967,9 +1070,10 @@ def write_fits(self, path: str, *, overwrite: bool = True) -> str: hdul.writeto(path, overwrite=overwrite) return path - # ------------------------------------------------------------------------- - # Plot helpers (optional) - # ------------------------------------------------------------------------- + @classmethod + def from_fits(cls): + #TODO + pass def plot_1d_pdfs( self, @@ -1063,15 +1167,7 @@ def corner_plot( dpi: int = 200, show: bool = False, ) -> str: - """ - Lightweight corner plot without external dependencies. - - Diagonal: 1D hist PDFs (weighted) - Off-diagonal: scatter of (subsampled) points colored by weight rank (simple) - - For serious usage, consider adding an optional dependency later (corner/arviz), - but this is a decent built-in baseline. - """ + """Build a corner plot""" npar = len(self.parameter_names) if npar == 0 or self.samples.size == 0: raise ValueError("No samples available to plot.") @@ -1087,21 +1183,25 @@ def corner_plot( S = self.samples[:, idx] w = self.weights[idx] - fig, axes = plt.subplots(npar, npar, figsize=(2.2 * npar, 2.2 * npar), constrained_layout=True) + fig, axes = plt.subplots( + npar, npar, figsize=(2.2 * npar, 2.2 * npar), + sharex="col", + constrained_layout=True) for i in range(npar): for j in range(npar): ax = axes[i, j] if i == j: x = self.samples[i, :] - xc, pdf = histogram_pdf_1d(x, self.weights, bins=bins) + edges, pdf = histogram_pdf_1d(x, self.weights, bins=bins) + xc = 0.5 * (edges[:-1] + edges[1:]) ax.plot(xc, pdf, lw=1.2) # mark mean/MAP ax.axvline(self.mean[i], lw=1.0, alpha=0.8) ax.axvline(self.map[i], lw=1.0, alpha=0.8) ax.set_yticks([]) elif i > j: - ax.scatter(S[j, :], S[i, :], s=2, alpha=0.25) + ax.hist2d(S[j, :], S[i, :], weights=w) else: ax.axis("off") @@ -1274,10 +1374,11 @@ def summarize_results( pdf1d: Dict[str, Dict[str, np.ndarray]] = {} if compute_1d: for i, nm in enumerate(names): - grid, hist_pdf = histogram_pdf_1d(samples[i, :], w, bins=pdf_bins_1d) - d = {"grid": grid, "hist_pdf": hist_pdf} + edges, hist_pdf = histogram_pdf_1d(samples[i, :], w, bins=pdf_bins_1d) + centers = 0.5 * (edges[:-1] + edges[1:]) + d = {"grid": centers, "edges": edges, "hist_pdf": hist_pdf} if kde_1d: - d["kde_pdf"] = kde_pdf_1d(samples[i, :], w, grid) + d["kde_pdf"] = kde_pdf_1d(samples[i, :], w, edges) pdf1d[nm] = d # 2D PDFs @@ -1476,100 +1577,6 @@ def weighted_quantiles(x: np.ndarray, w: np.ndarray, cdf = cdf / cdf[-1] return np.interp(qs, cdf, x) -def pdf_stats(edges: np.ndarray, pdf: np.ndarray, - quantiles=None, - find_multimodal: bool = False) -> dict: - """ - Compute summary stats from a discrete pdf over bin centers. - - Parameters - ---------- - edges : ndarray, shape (K,) - Bin edges. - pdf : ndarray, shape (K,) - PDF defined by the edges. - quantiles : tuple of float, optional - Quantiles to report (default 16, 50, 84 percent). - find_multimodal : bool, optional - If True, return rough modes by local-maximum search. - - Returns - ------- - stats : dict - Keys: mean, std, map, q, lo68, hi68, modes (optional). - """ - # Ensure normalization - norm = np.sum(pdf * np.diff(edges)) - pdf /= norm if norm > 0 else 1.0 - centers = 0.5 * (edges[:-1] + edges[1:]) - # Trapecium integrals - mean = np.sum(pdf * centers * np.diff(edges)) - var = np.sum(pdf * (centers - mean)**2 * np.diff(edges)) - std = var ** 0.5 - k_map = np.argmax(pdf * np.diff(edges)) - v_map = centers[k_map] - - cdf = np.cumsum(pdf * np.diff(edges)) - cdf = np.insert(cdf, 0, 0) - - if quantiles is not None: - qs = np.array(quantiles, float) - qvals = np.interp(qs, cdf, edges, left=edges[0], right=edges[-1]) - else: - qvals = None - - median = np.interp(0.5, cdf, edges, left=edges[0], right=edges[-1]) - lo68 = np.interp(0.16, cdf, edges, left=edges[0], right=edges[-1]) - hi68 = np.interp(0.84, cdf, edges, left=edges[0], right=edges[-1]) - - out = {"mean": mean, "std": std, "map": v_map, "q": qvals, - "median": median, "lo68": lo68, "hi68": hi68} - - if find_multimodal: - modes = [] - for i in range(1, len(pdf) - 1): - if pdf[i] > pdf[i - 1] and pdf[i] > pdf[i + 1]: - modes.append(centers[i]) - if not modes: - modes = [v_map] - out["modes"] = np.asarray(modes) - return out - - -def pit_from_discrete_posterior(z_true: np.ndarray, - posts: np.ndarray, - z_edges: np.ndarray) -> np.ndarray: - """ - Probability Integral Transform for discrete posteriors on bins. - - Assumes uniform density within each bin for within-bin interpolation. - - Parameters - ---------- - z_true : ndarray, shape (N,) - True values. - posts : ndarray, shape (N, K) - Row-normalised posteriors over K bins. - z_edges : ndarray, shape (K+1,) - Bin edges. - - Returns - ------- - pit : ndarray, shape (N,) - PIT values in [0, 1]. - """ - N, K = posts.shape - assert K == len(z_edges) - 1 - cdf_bins = np.cumsum(posts, axis=1) - j = np.clip(np.digitize(z_true, z_edges) - 1, 0, K - 1) - idx = np.arange(N) - below = np.where(j > 0, cdf_bins[idx, j - 1], 0.0) - widths = z_edges[1:] - z_edges[:-1] - frac = np.clip((z_true - z_edges[j]) / widths[j], 0.0, 1.0) - pit = below + posts[idx, j] * frac - return np.clip(pit, 0.0, 1.0) - - def photoz_metrics(z_true: np.ndarray, z_est: np.ndarray) -> dict: """ Standard photo-z metrics using delta z over 1+z. diff --git a/src/besta/sfh.py b/src/besta/sfh.py index a7830045..43749122 100644 --- a/src/besta/sfh.py +++ b/src/besta/sfh.py @@ -97,9 +97,21 @@ def make_ini(self, ini_file): Path to the output .ini file. """ logger.info("Making ini file: %s", ini_file) + + free_params = self.free_params.copy() + + if self.use_transforms: + logger.info("transforming default SFH values into latent variables") + if getattr(self, "sfh_bin_keys", None): + sfh_values = self.to_latent([free_params[k][1] for k in self.sfh_bin_keys]) + for key, val in zip(self.sfh_bin_keys, sfh_values): + free_params[key] = [-5, val, 5] + with open(ini_file, "w", encoding="utf-8") as file: + file.write(f"; Default prior file for SFH model: {str(self.__class__)}\n") + file.write(f"; use_transforms: {str(self.use_transforms)}\n") file.write(f"[{self.sect_name}]\n") - for key, val in self.free_params.items(): + for key, val in free_params.items(): if len(val) > 1: file.write(f"{key} = {val[0]} {val[1]} {val[2]}\n") else: @@ -169,15 +181,13 @@ class FixedTimeSFH(ZPowerLawMixin, SFHBase, PieceWiseSFHMixin): The SFH of a galaxy is modelled as a stepwise function where the free parameters correspond to the mass fraction formed on each bin. - Upon initializaiton, the free parameters are set between -8 to 0 in terms - of log(M/Msun). The starting point corresponds to the mass fraction formed - assuming a constant star formation history. + The default starting point for uniform priors corresponds to the mass + fraction formed assuming a constant star formation history. Attributes ---------- lookback_time : astropy.units.Quantity Lookback time bin edges. - """ def __init__(self, lookback_time_bins, *args, **kwargs): diff --git a/tests/_test_pymc_sampler.py b/tests/_test_pymc_sampler.py new file mode 100644 index 00000000..9a0b04f4 --- /dev/null +++ b/tests/_test_pymc_sampler.py @@ -0,0 +1,250 @@ +from __future__ import annotations + +import shutil +from pathlib import Path +from types import SimpleNamespace + +import numpy as np +import pytest + +from cosmosis.output import InMemoryOutput + +from besta import io +from besta.pipeline import MainPipeline +from besta.samplers.pymc_sampler import PymcSampler + + +def _write_ini(path: Path, text: str) -> Path: + path.write_text(text, encoding="utf-8") + return path + + +def test_pymc_sampler_smoke_with_dummy_pipeline(tmp_path): + class DummyPipeline: + def __init__(self): + self.varied_params = [object(), object()] + self.modules = [] + + def output_names(self): + # Varied params + one extra output. + return ["p0", "p1", "extra0"] + + def denormalize_vector(self, u): + # Map unit-cube to a bounded physical space. + return 4.0 * np.asarray(u, dtype=float) - 2.0 + + def run_results(self, p): + p = np.asarray(p, dtype=float) + post = -0.5 * float(np.dot(p, p)) + prior = -0.1 * float(np.dot(p, p)) + like = post - prior + extra = np.array([float(p.sum())], dtype=float) + return SimpleNamespace(post=post, prior=prior, like=like, extra=extra) + + ini_path = _write_ini( + tmp_path / "pymc_smoke.ini", + """ +[runtime] +sampler = pymc + +[pymc] +samples = 24 +nsteps = 8 +burn_fraction = 0.0 +chains = 1 +progressbar = F +seed = 11 +""".strip(), + ) + + output = InMemoryOutput() + sampler = PymcSampler(str(ini_path), DummyPipeline(), output) + sampler.config() + sampler.execute() + + assert sampler.is_converged() + assert sampler.num_samples > 0 + assert len(output.rows) == sampler.num_samples + + col_names = [c[0] for c in output.columns] + assert "prior" in col_names + assert "post" in col_names + assert "like" in col_names + + +def test_pymc_sampler_accepts_slice_step_method(tmp_path): + class DummyPipeline: + def __init__(self): + self.varied_params = [object(), object()] + self.modules = [] + + def output_names(self): + return ["p0", "p1", "extra0"] + + def denormalize_vector(self, u): + return 4.0 * np.asarray(u, dtype=float) - 2.0 + + def run_results(self, p): + p = np.asarray(p, dtype=float) + post = -0.5 * float(np.dot(p, p)) + prior = -0.1 * float(np.dot(p, p)) + like = post - prior + extra = np.array([float(p.sum())], dtype=float) + return SimpleNamespace(post=post, prior=prior, like=like, extra=extra) + + ini_path = _write_ini( + tmp_path / "pymc_slice.ini", + """ +[runtime] +sampler = pymc + +[pymc] +samples = 12 +nsteps = 4 +burn_fraction = 0.0 +chains = 1 +progressbar = F +seed = 5 +step_method = slice +""".strip(), + ) + + output = InMemoryOutput() + sampler = PymcSampler(str(ini_path), DummyPipeline(), output) + sampler.config() + sampler.execute() + + assert sampler.is_converged() + assert sampler.num_samples > 0 + + +def test_pymc_sampler_rejects_gradient_step_methods(tmp_path): + class DummyPipeline: + def __init__(self): + self.varied_params = [object(), object()] + self.modules = [] + + def output_names(self): + return ["p0", "p1", "extra0"] + + def denormalize_vector(self, u): + return 4.0 * np.asarray(u, dtype=float) - 2.0 + + def run_results(self, p): + p = np.asarray(p, dtype=float) + post = -0.5 * float(np.dot(p, p)) + prior = -0.1 * float(np.dot(p, p)) + like = post - prior + extra = np.array([float(p.sum())], dtype=float) + return SimpleNamespace(post=post, prior=prior, like=like, extra=extra) + + ini_path = _write_ini( + tmp_path / "pymc_nuts.ini", + """ +[runtime] +sampler = pymc + +[pymc] +samples = 6 +nsteps = 3 +burn_fraction = 0.0 +chains = 1 +progressbar = F +seed = 1 +step_method = nuts +""".strip(), + ) + + output = InMemoryOutput() + sampler = PymcSampler(str(ini_path), DummyPipeline(), output) + sampler.config() + with pytest.raises(ValueError, match="requires gradients"): + sampler.execute() + + +def test_pymc_sampler_full_besta_run(tmp_path): + cosmosis_exe = shutil.which("cosmosis") + if cosmosis_exe is None: + pytest.skip("cosmosis executable not available in PATH") + + module_path = tmp_path / "dummy_like_module.py" + module_path.write_text( + """ +def setup(options): + return {} + + +def execute(block, config): + p1 = block['parameters', 'p1'] + p2 = block['parameters', 'p2'] + like = -0.5 * (p1**2 + p2**2) + block['extra', 'sum_p'] = p1 + p2 + block['likelihoods', 'dummy_like'] = like + return 0 + + +def cleanup(config): + return 0 +""".strip(), + encoding="utf-8", + ) + + output_root = tmp_path / "pymc_full_run" + values_path = tmp_path / "values.ini" + values_path.write_text( + """ +[parameters] +p1 = -2.0 0.0 2.0 +p2 = -2.0 0.0 2.0 +""".strip(), + encoding="utf-8", + ) + + sampler_path = Path(__file__).resolve().parents[1] / "src" / "besta" / "samplers" / "pymc_sampler.py" + + config = { + "runtime": { + "sampler": "pymc", + "import_samplers": str(sampler_path), + }, + "pymc": { + "samples": 16, + "nsteps": 8, + "burn_fraction": 0.0, + "chains": 1, + "progressbar": False, + "seed": 3, + }, + "output": { + "filename": str(output_root), + "format": "text", + }, + "pipeline": { + "modules": "DummyLike", + "values": str(values_path), + "likelihoods": "dummy", + "quiet": "T", + "debug": "T", + "extra_output": "extra/sum_p", + }, + "DummyLike": { + "file": str(module_path), + }, + } + + runner = MainPipeline( + [config], + n_cores_list=[1], + ini_values_files=[str(values_path)], + ) + status = runner.execute_all(plot_result=False) + assert status == 0 + + results_file = Path(str(output_root) + ".txt") + assert results_file.exists() + + table = io.read_results_file(str(results_file)) + assert len(table) > 0 + assert "post" in table.colnames + assert "prior" in table.colnames + assert "like" in table.colnames diff --git a/tests/test_run.py b/tests/test_run.py index 3c2d8fb9..27bcd56d 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -111,6 +111,7 @@ def test_fit(self): "SFHModel": "ExponentialSFH", "velscale": 50.0, "ExtinctionLaw": "ccm89", + "use_features": "T", }} t0 = time() @@ -140,4 +141,4 @@ def test_postprocess(self): results = summarize_results(results.results_table) if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tutorials/fit_redshift/fit_jwst_redshift.ipynb b/tutorials/fit_redshift/fit_jwst_redshift.ipynb index 4cf2edee..623ebf60 100644 --- a/tutorials/fit_redshift/fit_jwst_redshift.ipynb +++ b/tutorials/fit_redshift/fit_jwst_redshift.ipynb @@ -521,7 +521,7 @@ ], "metadata": { "kernelspec": { - "display_name": "cosmo-env", + "display_name": "besta", "language": "python", "name": "python3" }, @@ -535,7 +535,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.10" + "version": "3.14.4" } }, "nbformat": 4, diff --git a/tutorials/fit_sdss_spectra/sdss_spectra.ipynb b/tutorials/fit_sdss_spectra/sdss_spectra.ipynb index ff4ffcf8..929df920 100644 --- a/tutorials/fit_sdss_spectra/sdss_spectra.ipynb +++ b/tutorials/fit_sdss_spectra/sdss_spectra.ipynb @@ -191,9 +191,11 @@ "metadata": {}, "outputs": [], "source": [ - "sdss_lsf_file = \"lsf_SDSS.dat\"\n", + "lsf_pixels = hdul[1].data[\"wdisp\"]\n", + "sdss_lsf_wl = lsf_pixels * np.gradient(wl.value)\n", + "sdss_lsf_fwhm = wl\n", + "# convert to FWHM in Angstrom at each wavelength accounting for each pixels varying dispersion\n", "emiles_lsf_file = \"e-miles_spectral_resolution.dat\"\n", - "sdss_lsf_wl, sdss_lsf_fwhm = np.loadtxt(sdss_lsf_file, unpack=True)\n", "emiles_lsf_wl, emiles_lsf_fwhm = np.loadtxt(emiles_lsf_file, unpack=True, usecols=(0, 1))\n", "\n", "wl_min = 3850\n", @@ -976,7 +978,7 @@ ], "metadata": { "kernelspec": { - "display_name": "cosmo-env", + "display_name": "besta", "language": "python", "name": "python3" }, @@ -990,7 +992,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.10" + "version": "3.14.4" } }, "nbformat": 4,