view Generate_figures.py @ 0:f40d05521dca draft default tip

planemo upload for repository https://github.com/esg-epfl-apc/tools-astro/tree/main/tools commit de01e3c02a26cd6353a6b9b6f8d1be44de8ccd54
author astroteam
date Fri, 25 Apr 2025 19:33:20 +0000
parents
children
line wrap: on
line source

#!/usr/bin/env python
# coding: utf-8

#!/usr/bin/env python

# This script is generated with nb2galaxy

# flake8: noqa

import json
import os
import shutil

from oda_api.json import CustomJSONEncoder

# oda:usesResource oda:CRBeamS3 .
# oda:CRBeamS3 a oda:S3 .
# oda:CRBeamS3 oda:resourceBindingEnvVarName "S3_CREDENTIALS" .

src_name = "NGC 1365"  # http://odahub.io/ontology#AstrophysicalObject

z_start = 0  # http://odahub.io/ontology#Float
Npart = 2000  # http://odahub.io/ontology#Integer ; oda:lower_limit 1 ; oda:upper_limit 100000
particle_type = "gamma"  # http://odahub.io/ontology#String ; oda:allowed_value "gamma","electron","proton"
Emax = 30  # http://odahub.io/ontology#Energy_TeV
Emin = 0.01  # http://odahub.io/ontology#Energy_TeV
EminSource = 1.0  # http://odahub.io/ontology#Energy_TeV
Gamma = 2.0  # http://odahub.io/ontology#Float
EGMF_fG = 10  # http://odahub.io/ontology#Float
lmaxEGMF_Mpc = 5  # http://odahub.io/ontology#Float
jet_half_size = 5.0  # oda:AngleDegrees
jet_direction = 0.0  # oda:AngleDegrees
psf = 1.0  # oda:AngleDegrees
window_size_RA = 2.0  # oda:AngleDegrees
window_size_DEC = 1.0  # oda:AngleDegrees
EBL = "Franceschini 2017"  # http://odahub.io/ontology#String ; oda:allowed_value "Franceschini 2017","Stecker 2016 lower limit","Stecker 2016 upper limit","Inoue 2012 Baseline","Inoue 2012 lower limit","Inoue 2012 upper limit"

_galaxy_wd = os.getcwd()

with open("inputs.json", "r") as fd:
    inp_dic = json.load(fd)
if "C_data_product_" in inp_dic.keys():
    inp_pdic = inp_dic["C_data_product_"]
else:
    inp_pdic = inp_dic
src_name = str(inp_pdic["src_name"])
z_start = float(inp_pdic["z_start"])
Npart = int(inp_pdic["Npart"])
particle_type = str(inp_pdic["particle_type"])
Emax = float(inp_pdic["Emax"])
Emin = float(inp_pdic["Emin"])
EminSource = float(inp_pdic["EminSource"])
Gamma = float(inp_pdic["Gamma"])
EGMF_fG = float(inp_pdic["EGMF_fG"])
lmaxEGMF_Mpc = float(inp_pdic["lmaxEGMF_Mpc"])
jet_half_size = float(inp_pdic["jet_half_size"])
jet_direction = float(inp_pdic["jet_direction"])
psf = float(inp_pdic["psf"])
window_size_RA = float(inp_pdic["window_size_RA"])
window_size_DEC = float(inp_pdic["window_size_DEC"])
EBL = str(inp_pdic["EBL"])

import os

from astropy import units as u
from astropy.coordinates import SkyCoord
from astropy.nddata import StdDevUncertainty
from astropy.table import Table
from gammapy.maps import Map, MapAxis
from oda_api.api import ProgressReporter
from oda_api.data_products import (
    LightCurveDataProduct,
    NumpyDataProduct,
    ODAAstropyTable,
    PictureProduct,
)
from specutils import Spectrum1D
from utils import find_redshift

if z_start <= 0:
    z_start = find_redshift(src_name)

source = SkyCoord.from_name(src_name, frame="icrs", parse=False, cache=True)

pr = ProgressReporter()
pr.report_progress(
    stage="Initialization",
    progress=0,
    substage="spectra",
    subprogress=30,
    message="some message",
)

import subprocess

import light_curve as lc
import numpy as np
from crbeam import CRbeam
from matplotlib import pyplot as plt
from spec_plot import plot_spec

EGMF = EGMF_fG * 1e-15

# internal units are eV
Emax *= 1e12
Emin *= 1e12
EminSource *= 1e12

background = {
    "Franceschini 2017": 12,
    "Stecker 2016 lower limit": 10,
    "Stecker 2016 upper limit": 11,
    "Inoue 2012 Baseline": 3,
    "Inoue 2012 lower limit": 4,
    "Inoue 2012 upper limit": 5,
}[EBL]

prog = CRbeam(
    z=z_start,
    nparticles=Npart,
    primary=particle_type,
    emax=Emax,
    emin=Emin,
    emin_source=EminSource,
    EGMF=EGMF,
    lmaxEGMF=lmaxEGMF_Mpc,
    background=background,
)
cmd = prog.command
cmd

# Here is one way to call CRbeam without global cache
# prog.run(force_overwrite=False)
# Here we call CRbeam with global cache
# prog.run_cached(overwrite_local_cache=True)
# to clear s3 cache run the following command
# prog.remove_cache()

# To report the progress we will split running CRbeam into 10 parts
n_steps = 10
# Initialize multistep simulation
data_exists = not prog.start_multistage_run(
    overwrite_local_cache=True, n_steps=n_steps
)
proceed = not data_exists

if proceed:
    for step in range(n_steps):
        pr.report_progress(
            stage="Running Simulation", progress=100 * step // n_steps
        )
        proceed = prog.simulation_step()
        # todo: report progress using rest API
    pr.report_progress(stage="Running Simulation", progress=100)

assert not proceed, "must be completed before this cell"
if not data_exists:
    prog.end_multistep_run()

def adjust_weights(mc_file, power):
    # converting weights to mimic required injection spectrum power
    header = ""
    with open(mc_file, "rt") as lines:
        for line in lines:
            if len(line) > 0 and line[0] == "#":
                header += line[1:].strip() + "\n"
            else:
                break
    weight_col = 2
    E_src_col = 12
    data = np.loadtxt(mc_file)
    weight = data[:, weight_col - 1]
    E_src = data[:, E_src_col - 1]
    orig_power = (
        prog.power_law
    )  # CRBeam is always called with fixed power=1 to optimize cache usage
    weight *= np.power(E_src / Emax, -(power - orig_power))
    output_file = f"{mc_file}_p{power}"
    np.savetxt(output_file, data, header=header.strip(), fmt="%.6g")
    return output_file

# this code will not execute program since files already exist on server
# prog.run(force_overwrite=False)

# one can upload cache explicitely by call
# prog.upload_cache()

pr.report_progress(stage="Building Plots and Tables", progress=0)

print(prog.output_path)
get_ipython().system("ls {prog.output_path}")   # noqa: F821

mc_file = prog.output_path + "/photon"

# how the data looks like
get_ipython().system("cat {mc_file} | awk 'NR<=5'")   # noqa: F821

if Emax != EminSource:
    mc_file = adjust_weights(mc_file, Gamma)

# how the data looks like
get_ipython().system("cat {mc_file} | awk 'NR<=5'")   # noqa: F821

subprocess.run(
    [
        "bash",
        os.path.join(os.environ.get("BASEDIR", os.getcwd()), "makeSpecE2.sh"),
        mc_file,
    ]
)

# rotating the beam

if EGMF > 0:
    from mc_rotate import mc_rotate

    mc_rotated_file = mc_rotate(
        mc_file, jet_half_size, jet_direction, psf_deg=psf
    )
else:
    mc_rotated_file = mc_file
mc_rotated_file

subprocess.run(
    [
        "bash",
        os.path.join(os.environ.get("BASEDIR", os.getcwd()), "makeSpecE2.sh"),
        mc_rotated_file,
    ]
)

# how the rotated data looks like
get_ipython().system("cat {mc_rotated_file} | awk 'NR<=5'")   # noqa: F821

# reading the source distance in Mpc from the data file
T_Mpc = lc.get_distance_Mpc(mc_file)
T_Mpc

# building the spectrum figure for total flux

spec_file = mc_file + ".spec"
spec_fig = plot_spec(
    spec_file, title="spectrum", ext="png", show=False, logscale=True
)
spec_image = PictureProduct.from_file(spec_fig)

# building the spectrum figure for the flux within PSF
spec_rotated_file = mc_rotated_file + ".spec"
spec_rotated_fig = plot_spec(
    spec_rotated_file,
    title="spectrum",
    Emin=Emin,
    Emax=Emax,
    ext="png",
    show=False,
    logscale=True,
)
spec_rotated_image = PictureProduct.from_file(spec_rotated_fig)

spec_rotated_file

lc_params = dict(
    logscale=True,
    max_t=-99,  # 8760000,
    n_points=30,
    psf=psf,
    min_n_particles=10,
    cut_0=True,
)

# building the light curve figure
if EGMF > 0:
    light_curve_fig = lc.make_plot(mc_rotated_file, **lc_params)
    light_curve_image = PictureProduct.from_file(light_curve_fig)
else:
    # to avoid possible problems with absent figure
    light_curve_image = PictureProduct.from_file(spec_fig)

l_curve = None
if EGMF > 0:
    delay, weights = lc.get_counts(mc_rotated_file, **lc_params)
    t, f, N = lc.light_curve(delay, weights, **lc_params)
    l_curve = LightCurveDataProduct.from_arrays(
        times=t * 3600.0,  # t is in hours
        fluxes=f,
        errors=f / np.sqrt(N),
        time_format="unix",
    )

def convert_to_ICRS(phi: np.array, theta: np.array, source: SkyCoord):
    # prime system has z-axes pointing the source centered at observer
    # it is obtained by rotation of source system by 180 degrees about x-axis
    # prime system coords have suffix "prime" (')
    # icrs system coords has empty suffix
    # TODO: add param ic_jet_plane_direction: SkyCoord - gives plane which contains jet
    # by definition in prime system the jet is in y'-z' plane and z' axes points from the source towards the observer
    # for simplicity we will first assume that prime frame is oriented as ICRS frame (use SkyOffsetFrame with rotation=0)

    # coordinates of unit direction vector in prime system

    # direction of event arrival at prime system
    z_prime = np.cos(
        theta / 180.0 * np.pi
    )  # (-1)*(-1) = 1 (rotating system and tacking oposite vector)
    r_xy_prime = np.sin(theta / 180.0 * np.pi)
    x_prime = -r_xy_prime * np.cos(
        phi / 180.0 * np.pi
    )  # (1)*(-1) = -1 (rotating system and tacking oposite vector)
    y_prime = r_xy_prime * np.sin(
        phi / 180.0 * np.pi
    )  # (-1)*(-1) = 1 (rotating system and tacking oposite vector)

    print("x',y',z' =", x_prime, y_prime, z_prime)

    # angle between z and z' axes
    delta1 = 90 * u.deg - source.dec

    print("source:", source.cartesian)
    print("delta1 = ", delta1)

    # rotating about y-axes

    x1 = x_prime * np.cos(delta1) + z_prime * np.sin(delta1)
    y1 = y_prime
    z1 = -x_prime * np.sin(delta1) + z_prime * np.cos(delta1)

    print("step 1: x,y,z =", x1, y1, z1)

    # rotation to -RA about z axis
    delta2 = source.ra
    x = x1 * np.cos(delta2) - y1 * np.sin(delta2)
    y = x1 * np.sin(delta2) + y1 * np.cos(delta2)
    z = z1

    print("x,y,z =", x, y, z)

    event_coords = SkyCoord(
        x=x, y=y, z=z, frame="icrs", representation_type="cartesian"
    ).spherical
    # print(event_coords.spherical)
    return event_coords

def LoadCubeTemplate(
    mc_file: str,
    source: SkyCoord,
    Emax=1e15,
    Emin=1e9,
    bins_per_decade=20,
    binsz=0.02,
    theta_mult=1,
    sec_component_mult=1,
    remove_straight=False,
    symmetric_split=0,
):
    pass

    mc_data = np.loadtxt(mc_file)
    E = mc_data[:, 0]
    print(f"dataset energy: {np.min(E)} <= E/eV <= {np.max(E)}")
    Theta = mc_data[:, 2] * theta_mult  # theta_mult is used for debug only
    print(f"dataset Theta: {np.min(Theta)} <= Theta <= {np.max(Theta)}")
    # idx = np.where((E >= Emin) & (E<=Emax) & (Theta<theta_max))[0]
    print(f"Filters: {Emin} <= E/eV <= {Emax}")
    condition = (E >= Emin) & (E <= Emax)
    if remove_straight:
        condition &= Theta > 0

    idx = np.where(condition)[0]

    print("filtered dataset length:", len(idx))

    E = E[idx]
    Theta = Theta[idx]
    w = mc_data[:, 1][idx]
    Phi = mc_data[:, 3][idx]
    # t = mc_data[:,5][idx]
    # t *= time_scale_hours
    if sec_component_mult != 1:  # sec_component_mult is used for debug only
        print("WARNING: Sec component multiplicity:", sec_component_mult)
        w[Theta > 0] *= sec_component_mult

    if len(idx) > 0:
        #     print(f'{np.min(t)} <= t/h <= {np.max(t)}')
        print(f"{np.min(E)} <= E <= {np.max(E)}")

    energy_axis_name = "energy_true"

    energy_axis = MapAxis.from_energy_bounds(
        Emin * u.eV,
        Emax * u.eV,
        nbin=bins_per_decade,
        name=energy_axis_name,
        per_decade=True,
    )

    m_cube = Map.create(
        binsz=binsz,
        width=(window_size_RA * u.deg, window_size_DEC * u.deg),
        frame="icrs",
        axes=[energy_axis],
        skydir=SkyCoord(source),
    )

    print(m_cube.geom)

    if len(idx) > 0:
        if symmetric_split > 1:
            Theta = np.repeat(Theta, symmetric_split)
            E = np.repeat(E, symmetric_split)
            w = np.repeat(w, symmetric_split) / symmetric_split
            Phi = np.random.random(len(Theta)) * 360

        sc = convert_to_ICRS(Phi, Theta, source)
        m_cube.fill_by_coord(
            {"lat": sc.lat, "lon": sc.lon, energy_axis_name: E * u.eV},
            weights=w,
        )

    return m_cube

def LoadTemplate4D(
    mc_file: str,
    source: SkyCoord,
    redshift,
    Emax=1e15,
    Emin=1e9,
    bins_per_decade=20,
    binsz=0.02,
    theta_mult=1,
    max_time_quantile=1.0,
    n_time_bins=100,
    symmetric_split=0,
):
    from astropy.cosmology import FlatLambdaCDM

    cosmo = FlatLambdaCDM(
        H0=67.8 * u.km / u.s / u.Mpc, Tcmb0=2.725 * u.K, Om0=(1 - 0.692)
    )
    time_scale_hours = float(
        ((cosmo.age(0) - cosmo.age(z_start)) / u.h).decompose()
    )

    mc_data = np.loadtxt(mc_file)
    E = mc_data[:, 0]

    print("dataset length:", len(mc_data))
    print(f"dataset energy: {np.min(E)} <= E/eV <= {np.max(E)}")
    Theta = mc_data[:, 2] * theta_mult  # theta_mult is used for debug only
    print(f"dataset Theta: {np.min(Theta)} <= Theta <= {np.max(Theta)}")
    # idx = np.where((E >= Emin) & (E<=Emax) & (Theta<theta_max))[0]

    idx = np.where((E >= Emin) & (E <= Emax))[0]

    print(f"Filters: {Emin} <= E/eV <= {Emax}")
    print("filtered dataset length:", len(idx))

    E = E[idx]
    Theta = Theta[idx]
    w = mc_data[:, 1][idx]
    Phi = mc_data[:, 3][idx]
    t = mc_data[:, 5][idx]
    t *= time_scale_hours
    min_time = np.min(t)
    if max_time_quantile >= 1.0:
        max_time = np.max(t)
    else:
        max_time = np.quantile(t, max_time_quantile)

    if max_time == min_time:
        max_time = min_time + 1e-10

    idx = np.where(t <= max_time)[0]
    print(f"Filters: {Emin} <= E/eV <= {Emax}, t/h <= {max_time}")
    print("filtered dataset length:", len(idx))

    E = E[idx]
    Theta = Theta[idx]
    w = w[idx]
    Phi = Phi[idx]
    t = t[idx]

    energy_axis_name = "energy_true"

    energy_axis = MapAxis.from_energy_bounds(
        Emin * u.eV,
        Emax * u.eV,
        nbin=bins_per_decade,
        name=energy_axis_name,
        per_decade=True,
    )

    from astropy.time import Time

    reference_time = Time(0, format="unix")

    delta = max_time - min_time
    (min_time + delta * np.linspace(0, 1, n_time_bins + 1)) * u.h
    # time_axis = TimeMapAxis.from_time_edges(
    #     time_min=time_edges[:-1],
    #     time_max=time_edges[1:],
    #     interp="lin",
    #     unit=u.h,
    #     name='time',
    #     reference_time=reference_time
    #  )

    time_axis = MapAxis.from_bounds(
        min_time, max_time, n_time_bins, name="time", unit=u.h
    )

    # time_axis = TimeMapAxis(time_edges[:-1], time_edges[1:], reference_time, name='time', interp='lin')

    # time_axis = TimeMapAxis.from_time_bounds(min_time, max_time, n_time_bins, unit=u.h, name='time')

    map4d = Map.create(
        binsz=binsz,
        width=(window_size_RA * u.deg, window_size_DEC * u.deg),
        frame="icrs",
        axes=[energy_axis, time_axis],
        skydir=SkyCoord(source),
    )

    print(map4d.geom)

    if len(idx) > 0:
        if symmetric_split > 1:
            Theta = np.repeat(Theta, symmetric_split)
            E = np.repeat(E, symmetric_split)
            t = np.repeat(t, symmetric_split)
            w = np.repeat(w, symmetric_split) / symmetric_split
            Phi = np.random.random(len(Theta)) * 360

        sc = convert_to_ICRS(Phi, Theta, source)
        map4d.fill_by_coord(
            {
                "lat": sc.lat,
                "lon": sc.lon,
                energy_axis_name: E * u.eV,
                "time": t * u.h,
            },
            weights=w,
        )

    return map4d

bins_per_decade = 20

symmetric_split = 0 if jet_direction != 0 else 100

map3d = LoadCubeTemplate(
    mc_rotated_file,
    source=source,
    Emin=Emin,
    Emax=Emax,
    bins_per_decade=bins_per_decade,
    symmetric_split=symmetric_split,
)

map3d.write("map3d.fits", overwrite=True)

map4d = LoadTemplate4D(
    mc_rotated_file,
    source=source,
    redshift=z_start,
    Emin=Emin,
    Emax=Emax,
    bins_per_decade=bins_per_decade,
    symmetric_split=symmetric_split,
)
map4d.write("map4d.fits", overwrite=True)

d = np.genfromtxt(spec_file)
ee = d[:, 0]
ff = d[:, 1]
ff_err = ff / np.sqrt(d[:, 2])
plt.errorbar(ee, ff, yerr=ff_err, label="total flux")
d = np.genfromtxt(spec_rotated_file)
ee1 = d[:, 0]
ff1 = d[:, 1]
ff_err1 = ff1 / np.sqrt(d[:, 2])
plt.errorbar(ee1, ff1, yerr=ff_err1, label="PSF flux")
plt.xscale("log")
plt.yscale("log")
plt.xlabel("Energy, eV")
plt.ylabel("$E^2dN/dE$, arbitrary units")
plt.legend(loc="lower right")
plt.savefig("Spectrum.png", format="png", bbox_inches="tight")

bin_image = PictureProduct.from_file("Spectrum.png")

data = [ee, ff, ff_err]
names = ("Energy", "Total_flux", "Total_flux_err")
table = ODAAstropyTable(Table(data, names=names))
data = [ee, ff, ff_err]
names = ("Energy", "PSF_flux", "PSF_flux_err")
table1 = ODAAstropyTable(Table(data, names=names))

flux_unit = u.eV / u.cm / u.cm / u.s / u.sr
spec = Spectrum1D(
    spectral_axis=ee * u.eV,
    flux=ff * flux_unit,
    uncertainty=StdDevUncertainty(ff_err),
)
spec.write("spec.fits", overwrite=True)
dp = NumpyDataProduct.from_fits_file("spec.fits")
spec_rotated = Spectrum1D(
    spectral_axis=ee1 * u.eV,
    flux=ff1 * flux_unit,
    uncertainty=StdDevUncertainty(ff_err1),
)
spec_rotated.write("spec_rotated.fits", overwrite=True)
dp_rotated = NumpyDataProduct.from_fits_file("spec_rotated.fits")
map3d = NumpyDataProduct.from_fits_file("map3d.fits")
map4d = NumpyDataProduct.from_fits_file("map4d.fits")

pr.report_progress(stage="Building Plots and Tables", progress=100)

spectrum_png = bin_image  # http://odahub.io/ontology#ODAPictureProduct
light_curve_png = (
    light_curve_image  # http://odahub.io/ontology#ODAPictureProduct
)
total_spectrum_table = table  # http://odahub.io/ontology#ODAAstropyTable
psf_spectrum_table = table1  # http://odahub.io/ontology#ODAAstropyTable
lc_result = l_curve  # http://odahub.io/ontology#LightCurve
spectrum = dp  # http://odahub.io/ontology#Spectrum
spectrum_rotated = dp_rotated  # http://odahub.io/ontology#Spectrum
map3d = map3d  # http://odahub.io/ontology#Spectrum
map4d = map4d  # http://odahub.io/ontology#Spectrum

# spec_png = spec_image # http://odahub.io/ontology#ODAPictureProduct
# spec_rotated_png = spec_rotated_image # http://odahub.io/ontology#ODAPictureProduct

# output gathering
_galaxy_meta_data = {}
_oda_outs = []
_oda_outs.append(
    (
        "out_Generate_figures_spectrum_png",
        "spectrum_png_galaxy.output",
        spectrum_png,
    )
)
_oda_outs.append(
    (
        "out_Generate_figures_light_curve_png",
        "light_curve_png_galaxy.output",
        light_curve_png,
    )
)
_oda_outs.append(
    (
        "out_Generate_figures_total_spectrum_table",
        "total_spectrum_table_galaxy.output",
        total_spectrum_table,
    )
)
_oda_outs.append(
    (
        "out_Generate_figures_psf_spectrum_table",
        "psf_spectrum_table_galaxy.output",
        psf_spectrum_table,
    )
)
_oda_outs.append(
    ("out_Generate_figures_lc_result", "lc_result_galaxy.output", lc_result)
)
_oda_outs.append(
    ("out_Generate_figures_spectrum", "spectrum_galaxy.output", spectrum)
)
_oda_outs.append(
    (
        "out_Generate_figures_spectrum_rotated",
        "spectrum_rotated_galaxy.output",
        spectrum_rotated,
    )
)
_oda_outs.append(("out_Generate_figures_map3d", "map3d_galaxy.output", map3d))
_oda_outs.append(("out_Generate_figures_map4d", "map4d_galaxy.output", map4d))

for _outn, _outfn, _outv in _oda_outs:
    _galaxy_outfile_name = os.path.join(_galaxy_wd, _outfn)
    if isinstance(_outv, str) and os.path.isfile(_outv):
        shutil.move(_outv, _galaxy_outfile_name)
        _galaxy_meta_data[_outn] = {"ext": "_sniff_"}
    elif getattr(_outv, "write_fits_file", None):
        _outv.write_fits_file(_galaxy_outfile_name)
        _galaxy_meta_data[_outn] = {"ext": "fits"}
    elif getattr(_outv, "write_file", None):
        _outv.write_file(_galaxy_outfile_name)
        _galaxy_meta_data[_outn] = {"ext": "_sniff_"}
    else:
        with open(_galaxy_outfile_name, "w") as fd:
            json.dump(_outv, fd, cls=CustomJSONEncoder)
        _galaxy_meta_data[_outn] = {"ext": "json"}

with open(os.path.join(_galaxy_wd, "galaxy.json"), "w") as fd:
    json.dump(_galaxy_meta_data, fd)
print("*** Job finished successfully ***")