def test_parameter_dict(tmp_path): from datetime import date sim = lbs.Simulation( parameters={ "general": { "a": 10, "b": 20.0, "c": False, "subtable": { "d": date(2020, 7, 1), "e": "Hello, world!" }, } }) assert not sim.parameter_file assert isinstance(sim.parameters, dict) assert "general" in sim.parameters assert sim.parameters["general"]["a"] == 10 assert sim.parameters["general"]["b"] == 20.0 assert not sim.parameters["general"]["c"] assert "subtable" in sim.parameters["general"] assert sim.parameters["general"]["subtable"]["d"] == date(2020, 7, 1) assert sim.parameters["general"]["subtable"]["e"] == "Hello, world!" try: sim = lbs.Simulation(parameter_file="dummy", parameters={"a": 10}) assert False, "Simulation object should have asserted" except AssertionError: pass
def test_parameter_file(): from datetime import date with NamedTemporaryFile(mode="wt", delete=False) as conf_file: conf_file_name = conf_file.name conf_file.write("""# test_parameter_file [simulation] start_time = "2020-01-01T00:00:00" duration_s = 11.0 description = "Dummy description" [general] a = 10 b = 20.0 c = false [general.subtable] d = 2020-07-01 e = "Hello, world!" """) with TemporaryDirectory() as tmpdirname: sim = lbs.Simulation(base_path=tmpdirname, parameter_file=conf_file_name) assert isinstance(sim.parameter_file, pathlib.Path) assert isinstance(sim.parameters, dict) assert "simulation" in sim.parameters assert isinstance(sim.start_time, astropy.time.Time) assert sim.duration_s == 11.0 assert sim.description == "Dummy description" assert "general" in sim.parameters assert sim.parameters["general"]["a"] == 10 assert sim.parameters["general"]["b"] == 20.0 assert not sim.parameters["general"]["c"] assert "subtable" in sim.parameters["general"] assert sim.parameters["general"]["subtable"]["d"] == date(2020, 7, 1) assert sim.parameters["general"]["subtable"]["e"] == "Hello, world!" # Check that the code does not complain if the output directory is # the same as the one containing the parameter file sim = lbs.Simulation(base_path=Path(conf_file_name).parent, parameter_file=conf_file_name) os.unlink(conf_file_name)
def test_simulation_pointings_polangle(tmp_path): sim = lbs.Simulation(base_path=tmp_path / "simulation_dir", start_time=0.0, duration_s=61.0) fakedet = create_fake_detector(sampling_rate_hz=50.0) sim.create_observations(detectors=[fakedet], num_of_obs_per_detector=1, split_list_over_processes=False) assert len(sim.observations) == 1 obs = sim.observations[0] sstr = lbs.SpinningScanningStrategy(spin_sun_angle_rad=0.0, precession_rate_hz=0.0, spin_rate_hz=1.0 / 60) sim.generate_spin2ecl_quaternions(scanning_strategy=sstr, delta_time_s=0.5) instr = lbs.InstrumentInfo(spin_boresight_angle_rad=0.0) pointings_and_polangle = lbs.get_pointings( obs, spin2ecliptic_quats=sim.spin2ecliptic_quats, detector_quats=np.array([[0.0, 0.0, 0.0, 1.0]]), bore2spin_quat=instr.bore2spin_quat, ) polangle = pointings_and_polangle[..., 2] # Check that the polarization angle scans every value between -π # and +π assert np.allclose(np.max(polangle), np.pi, atol=0.01) assert np.allclose(np.min(polangle), -np.pi, atol=0.01) # Simulate the generation of a report sim.flush()
def test_simulation_pointings_mjd(tmp_path): sim = lbs.Simulation( base_path=tmp_path / "simulation_dir", start_time=Time("2020-01-01T00:00:00"), duration_s=130.0, ) fakedet = create_fake_detector() sim.create_observations(detectors=[fakedet], num_of_obs_per_detector=2, split_list_over_processes=False) sstr = lbs.SpinningScanningStrategy(spin_sun_angle_rad=10.0, precession_rate_hz=10.0, spin_rate_hz=0.1) sim.generate_spin2ecl_quaternions(scanning_strategy=sstr, delta_time_s=60.0) instr = lbs.InstrumentInfo(spin_boresight_angle_rad=np.deg2rad(20.0)) for idx, obs in enumerate(sim.observations): pointings_and_polangle = lbs.get_pointings( obs, spin2ecliptic_quats=sim.spin2ecliptic_quats, detector_quats=np.array([[0.0, 0.0, 0.0, 1.0]]), bore2spin_quat=instr.bore2spin_quat, ) filename = Path( __file__).parent / f"reference_obs_pointings{idx:03d}.npy" reference = np.load(filename, allow_pickle=False) assert np.allclose(pointings_and_polangle, reference)
def test_imo_in_report(tmp_path): curpath = pathlib.Path(__file__).parent imo = lbs.Imo(flatfile_location=curpath / "mock_imo") sim = lbs.Simulation( base_path=tmp_path / "simulation_dir", name="My simulation", description="Lorem ipsum", imo=imo, ) entity_uuid = UUID("dd32cb51-f7d5-4c03-bf47-766ce87dc3ba") _ = sim.imo.query(f"/entities/{entity_uuid}") quantity_uuid = UUID("e9916db9-a234-4921-adfd-6c3bb4f816e9") _ = sim.imo.query(f"/quantities/{quantity_uuid}") data_file_uuid = UUID("37bb70e4-29b2-4657-ba0b-4ccefbc5ae36") _ = sim.imo.query(f"/data_files/{data_file_uuid}") # This data file is an older version of 37bb70e4 data_file_uuid = UUID("bd8e16eb-2e9d-46dd-a971-f446e953b9dc") _ = sim.imo.query(f"/data_files/{data_file_uuid}") html_file = sim.flush() assert isinstance(html_file, pathlib.Path) assert html_file.exists()
def test_make_bin_map_api_simulation(tmp_path): # We should add a more meaningful observation: # Currently this test just shows the interface sim = lbs.Simulation(base_path=tmp_path / "tut04", start_time=0.0, duration_s=86400.0) sim.generate_spin2ecl_quaternions( scanning_strategy=lbs.SpinningScanningStrategy( spin_sun_angle_rad=np.radians(30), # CORE-specific parameter spin_rate_hz=0.5 / 60, # Ditto # We use astropy to convert the period (4 days) in # minutes, the unit expected for the precession period precession_rate_hz=1 / (4 * u.day).to("s").value, )) instr = lbs.InstrumentInfo(name="core", spin_boresight_angle_rad=np.radians(65)) det = lbs.DetectorInfo(name="foo", sampling_rate_hz=10) obss = sim.create_observations(detectors=[det]) pointings = lbs.get_pointings( obss[0], sim.spin2ecliptic_quats, detector_quats=[det.quat], bore2spin_quat=instr.bore2spin_quat, ) nside = 64 obss[0].pixind = hp.ang2pix(nside, pointings[..., 0], pointings[..., 1]) obss[0].psi = pointings[..., 2] mapping.make_bin_map(obss, nside)
def test_mbs(): try: # PySM3 produces a lot of warnings when it downloads stuff from Internet warnings.filterwarnings("ignore") with NamedTemporaryFile(mode="w+t", suffix=".toml") as simfile: with TemporaryDirectory() as outdir: simfile.write(PARAMETER_FILE.format(output_directory=outdir)) simfile.flush() sim = lbs.Simulation(base_path=outdir, parameter_file=simfile.name) myinst = {} myinst["mock"] = { "bandcenter_ghz": 140.0, "bandwidth_ghz": 42.0, "fwhm_arcmin": 30.8, "p_sens_ukarcmin": 6.39, } mbs = lbs.Mbs(sim, sim.parameters["map_based_sims"], instrument=myinst) (maps, saved_maps) = mbs.run_all() curpath = Path(__file__).parent map_ref = hp.read_map(curpath / "reference_mbs.fits", (0, 1, 2)) assert np.allclose(maps["mock"], map_ref, atol=1e-6) finally: # Reset the standard warnings filter warnings.resetwarnings()
def test_simulation_pointings_mjd(tmp_path): sim = lbs.Simulation( base_path=tmp_path / "simulation_dir", start_time=Time("2020-01-01T00:00:00"), duration_s=130.0, ) fakedet = create_fake_detector() sim.create_observations( detectors=[fakedet], num_of_obs_per_detector=2, distribute=False, ) sstr = lbs.SpinningScanningStrategy( spin_sun_angle_deg=10.0, precession_period_min=10.0, spin_rate_rpm=0.1, ) sim.generate_spin2ecl_quaternions(scanning_strategy=sstr, delta_time_s=60.0) instr = lbs.Instrument(spin_boresight_angle_deg=20.0) for obs in sim.observations: pointings_and_polangle = obs.get_pointings( spin2ecliptic_quats=sim.spin2ecliptic_quats, detector_quats=np.array([[0.0, 0.0, 0.0, 1.0]]), bore2spin_quat=instr.bore2spin_quat, )
def test_distribution(): comm_world = lbs.MPI_COMM_WORLD if comm_world.rank == 0: print(f"MPI configuration: {lbs.MPI_CONFIGURATION}") sim = lbs.Simulation(mpi_comm=comm_world) det1 = lbs.Detector(name="pol01", beam_z=[0, 0, 1], sampfreq_hz=5, simulation=sim) det2 = lbs.Detector(name="pol02", beam_z=[0, 0, 1], sampfreq_hz=50, simulation=sim) sim.create_observations( [det1, det2], num_of_obs_per_detector=10, start_time=0.0, duration_s=86400.0 * 10, ) assert ( sim.observations ), "No observations have been defined for process {lbs.MPI_RANK + 1}/{lbs.MPI_SIZE}" if comm_world.size > 1: allobs = comm_world.allreduce( [x.detector.name for x in sim.observations]) else: allobs = sim.observations assert len(allobs) == 20
def test_parameter_file(tmp_path): from datetime import date conf_file = pathlib.Path(tmp_path) / "configuration.toml" with conf_file.open("wt") as outf: outf.write("""[simulation] start_time = "2020-01-01T00:00:00" duration_s = 11.0 description = "Dummy description" [general] a = 10 b = 20.0 c = false [general.subtable] d = 2020-07-01 e = "Hello, world!" """) sim = lbs.Simulation(parameter_file=conf_file) assert isinstance(sim.parameter_file, pathlib.Path) assert isinstance(sim.parameters, dict) assert "simulation" in sim.parameters assert isinstance(sim.start_time, astropy.time.Time) assert sim.duration_s == 11.0 assert sim.description == "Dummy description" assert "general" in sim.parameters assert sim.parameters["general"]["a"] == 10 assert sim.parameters["general"]["b"] == 20.0 assert not sim.parameters["general"]["c"] assert "subtable" in sim.parameters["general"] assert sim.parameters["general"]["subtable"]["d"] == date(2020, 7, 1) assert sim.parameters["general"]["subtable"]["e"] == "Hello, world!" # Check that the code does not complain if the output directory is # the same as the one containing the parameter file sim = lbs.Simulation(base_path=tmp_path, parameter_file=conf_file)
def test_distribute_observation(tmp_path): sim = lbs.Simulation(base_path=tmp_path / "simulation_dir", start_time=1.0, duration_s=11.0) det = lbs.Detector("dummy", sampling_rate_hz=15) obs_list = sim.create_observations(detectors=[det], num_of_obs_per_detector=5) assert len(obs_list) == 5 assert int(obs_list[-1].get_times()[-1] - obs_list[0].get_times()[0]) == 10 assert sum([o.n_samples for o in obs_list]) == sim.duration_s * det.sampling_rate_hz
def test_markdown_report(tmp_path): sim = lbs.Simulation( base_path=tmp_path / "simulation_dir", name="My simulation", description="Lorem ipsum", start_time=1.0, duration_s=3600.0, ) output_file = sim.write_healpix_map(filename="test.fits.gz", pixels=np.zeros(12)) assert isinstance(output_file, pathlib.Path) assert output_file.exists() sim.append_to_report( """Here is a plot: ![](myplot.png) And here are the data points: {% for sample in data_points -%} - {{ sample }} {% endfor %} """, figures=[(MockPlot(), "myplot.png")], data_points=[0, 1, 2], ) reference = """# My simulation Lorem ipsum The simulation starts at t0=1.0 and lasts 3600.0 seconds. [TOC] Here is a plot: ![](myplot.png) And here are the data points: - 0 - 1 - 2 """ print(sim.report) assert reference.strip() in sim.report.strip() sim.flush()
def test_simulation_pointings_spinning(tmp_path): sim = lbs.Simulation( base_path=tmp_path / "simulation_dir", start_time=0.0, duration_s=61.0, ) fakedet = create_fake_detector(sampling_rate_hz=50.0) sim.create_observations( detectors=[fakedet], num_of_obs_per_detector=1, distribute=False, ) assert len(sim.observations) == 1 obs = sim.observations[0] sstr = lbs.SpinningScanningStrategy( spin_sun_angle_deg=0.0, precession_period_min=0.0, spin_rate_rpm=1.0, ) sim.generate_spin2ecl_quaternions(scanning_strategy=sstr, delta_time_s=0.5) instr = lbs.Instrument(spin_boresight_angle_deg=15.0) pointings_and_polangle = obs.get_pointings( spin2ecliptic_quats=sim.spin2ecliptic_quats, detector_quats=np.array([[0.0, 0.0, 0.0, 1.0]]), bore2spin_quat=instr.bore2spin_quat, ) colatitude = pointings_and_polangle[..., 0] with open("spin2ecliptic_quats.txt", "wt") as outf: for i in range(sim.spin2ecliptic_quats.quats.shape[0]): print(*sim.spin2ecliptic_quats.quats[i, :], file=outf) with open("pointings.txt", "wt") as outf: import healpy for i in range(pointings_and_polangle.shape[1]): print( *healpy.ang2vec(pointings_and_polangle[0, i, 0], pointings_and_polangle[0, i, 1]), file=outf, ) # Check that the colatitude does not depart more than ±15° from # the Ecliptic assert np.allclose(np.rad2deg(np.max(colatitude)), 90 + 15, atol=0.01) assert np.allclose(np.rad2deg(np.min(colatitude)), 90 - 15, atol=0.01) sim.flush()
def __write_complex_observation(tmp_path, use_mjd: bool): start_time = AstroTime("2021-01-01") if use_mjd else 0 time_span_s = 60 sampling_hz = 10 sim = lbs.Simulation( base_path=tmp_path, start_time=start_time, duration_s=time_span_s, ) scanning = lbs.SpinningScanningStrategy( spin_sun_angle_rad=0.785_398_163_397_448_3, precession_rate_hz=8.664_850_513_998_931e-05, spin_rate_hz=0.000_833_333_333_333_333_4, start_time=start_time, ) spin2ecliptic_quats = scanning.generate_spin2ecl_quaternions( start_time, time_span_s, delta_time_s=1.0) instr = lbs.InstrumentInfo( boresight_rotangle_rad=0.0, spin_boresight_angle_rad=0.872_664_625_997_164_8, spin_rotangle_rad=3.141_592_653_589_793, ) det = lbs.DetectorInfo( name="Dummy detector", sampling_rate_hz=sampling_hz, bandcenter_ghz=100.0, quat=[0.0, 0.0, 0.0, 1.0], ) sim.create_observations(detectors=[det]) obs = sim.observations[0] obs.tod = np.random.random(obs.tod.shape) obs.pointings = lbs.scanning.get_pointings( obs, spin2ecliptic_quats=spin2ecliptic_quats, detector_quats=[det.quat], bore2spin_quat=instr.bore2spin_quat, ) obs.local_flags = np.zeros(obs.tod.shape, dtype="uint16") obs.local_flags[0, 12:15] = 1 obs.global_flags = np.zeros(obs.tod.shape[1], dtype="uint32") obs.global_flags[12:15] = 15 return obs, det, lbs.write_observations(sim=sim, subdir_name="")
def test_duration_units_in_parameter_file(tmp_path): conf_file = pathlib.Path(tmp_path) / "configuration.toml" with conf_file.open("wt") as outf: outf.write("""[simulation] start_time = "2020-01-01T00:00:00" duration_s = "1 day" """) sim = lbs.Simulation(parameter_file=conf_file) assert "simulation" in sim.parameters assert isinstance(sim.start_time, astropy.time.Time) assert sim.duration_s == 86400.0
def test_simulation_two_detectors(): sim = lbs.Simulation(start_time=0.0, duration_s=86400.0) # Two detectors, the second rotated by 45° quaternions = [ np.array([0.0, 0.0, 0.0, 1.0]), np.array([0.0, 0.0, 1.0, 1.0]) / np.sqrt(2), ] fakedet1 = create_fake_detector(sampling_rate_hz=1 / 3600, quat=quaternions[0]) fakedet2 = create_fake_detector(sampling_rate_hz=1 / 3600, quat=quaternions[1]) sim.create_observations( detectors=[fakedet1, fakedet2], num_of_obs_per_detector=1, split_list_over_processes=False, ) assert len(sim.observations) == 1 obs = sim.observations[0] # The spacecraft stands still in L2, with no spinning nor precession sstr = lbs.SpinningScanningStrategy(spin_sun_angle_rad=0.0, precession_rate_hz=0.0, spin_rate_hz=0.0) sim.generate_spin2ecl_quaternions(sstr, delta_time_s=60.0) assert sim.spin2ecliptic_quats.quats.shape == (24 * 60 + 1, 4) instr = lbs.InstrumentInfo(spin_boresight_angle_rad=0.0) pointings_and_polangle = lbs.get_pointings( obs, spin2ecliptic_quats=sim.spin2ecliptic_quats, detector_quats=quaternions, bore2spin_quat=instr.bore2spin_quat, ) assert pointings_and_polangle.shape == (2, 24, 3) assert np.allclose(pointings_and_polangle[0, :, 0], pointings_and_polangle[1, :, 0]) assert np.allclose(pointings_and_polangle[0, :, 1], pointings_and_polangle[1, :, 1]) # The ψ angle should differ by 45° assert np.allclose( np.abs(pointings_and_polangle[0, :, 2] - pointings_and_polangle[1, :, 2]), np.pi / 2, )
def test_simulation_pointings_still(): sim = lbs.Simulation(start_time=0.0, duration_s=86400.0) fakedet = create_fake_detector(sampling_rate_hz=1 / 3600) sim.create_observations( detectors=[fakedet], num_of_obs_per_detector=1, distribute=False, ) assert len(sim.observations) == 1 obs = sim.observations[0] # The spacecraft stands still in L2, with no spinning nor precession sstr = lbs.SpinningScanningStrategy( spin_sun_angle_deg=0.0, precession_period_min=0.0, spin_rate_rpm=0.0, ) sim.generate_spin2ecl_quaternions(sstr, delta_time_s=60.0) assert sim.spin2ecliptic_quats.quats.shape == (24 * 60 + 1, 4) instr = lbs.Instrument(spin_boresight_angle_deg=0.0) # Move the Z vector manually using the last quaternion and check # that it's rotated by 1/365.25 of a complete circle boresight = np.empty(3) lbs.rotate_z_vector(boresight, *sim.spin2ecliptic_quats.quats[-1, :]) assert np.allclose(np.arctan2(boresight[1], boresight[0]), 2 * np.pi / 365.25) # Now redo the calculation using Observation.get_pointings pointings_and_polangle = obs.get_pointings( spin2ecliptic_quats=sim.spin2ecliptic_quats, detector_quats=np.array([[0.0, 0.0, 0.0, 1.0]]), bore2spin_quat=instr.bore2spin_quat, ) colatitude = pointings_and_polangle[..., 0] longitude = pointings_and_polangle[..., 1] polangle = pointings_and_polangle[..., 2] assert np.allclose(colatitude, np.pi / 2), colatitude assert np.allclose(np.abs(polangle), np.pi / 2), polangle # The longitude should have changed by a fraction 23 hours / # 365.25 days of a complete circle (we have 24 samples, from t = 0 # to t = 23 hr) assert np.allclose(np.abs(longitude[..., -1] - longitude[..., 0]), 2 * np.pi * 23 / 365.25 / 24)
def test_duration_units_in_parameter_file(): with NamedTemporaryFile(mode="wt", delete=False) as conf_file: conf_file_name = conf_file.name conf_file.write("""# test_duration_units_in_parameter_file [simulation] start_time = "2020-01-01T00:00:00" duration_s = "1 day" """) with TemporaryDirectory() as tmpdirname: sim = lbs.Simulation(base_path=tmpdirname, parameter_file=conf_file_name) assert "simulation" in sim.parameters assert isinstance(sim.start_time, astropy.time.Time) assert sim.duration_s == 86400.0
def test_scanning_quaternions(tmp_path): sim = lbs.Simulation( base_path=tmp_path / "simulation_dir", start_time=0.0, duration_s=61.0, ) fakedet = create_fake_detector(sampling_rate_hz=50.0) sim.create_observations( detectors=[fakedet], num_of_obs_per_detector=1, distribute=False, ) assert len(sim.observations) == 1 obs = sim.observations[0] sstr = lbs.SpinningScanningStrategy( spin_sun_angle_deg=0.0, precession_period_min=0.0, spin_rate_rpm=1.0, ) sim.generate_spin2ecl_quaternions(scanning_strategy=sstr, delta_time_s=0.5) instr = lbs.Instrument(spin_boresight_angle_deg=15.0) detector_quat = np.array([[0.0, 0.0, 0.0, 1.0]]) det2ecl_quats = obs.get_det2ecl_quaternions( spin2ecliptic_quats=sim.spin2ecliptic_quats, detector_quats=detector_quat, bore2spin_quat=instr.bore2spin_quat, ) ecl2det_quats = obs.get_ecl2det_quaternions( spin2ecliptic_quats=sim.spin2ecliptic_quats, detector_quats=detector_quat, bore2spin_quat=instr.bore2spin_quat, ) identity = np.array([0, 0, 0, 1]) det2ecl_quats = det2ecl_quats.reshape(-1, 4) ecl2det_quats = ecl2det_quats.reshape(-1, 4) for i in range(det2ecl_quats.shape[0]): # Check that the two quaternions (ecl2det and det2ecl) are # actually one the inverse of the other quat = np.copy(det2ecl_quats[i, :]) lbs.quat_right_multiply(quat, *ecl2det_quats[i, :]) assert np.allclose(quat, identity)
def test_healpix_map_write(tmp_path): sim = lbs.Simulation(base_path=tmp_path / "simulation_dir") output_file = sim.write_healpix_map(filename="test.fits.gz", pixels=np.zeros(12)) assert isinstance(output_file, pathlib.Path) assert output_file.exists() sim.append_to_report( """Here is a plot: ![](myplot.png) """, [(MockPlot(), "myplot.png")], ) sim.flush()
def test_simulation_pointings_spinning(tmp_path): sim = lbs.Simulation(base_path=tmp_path / "simulation_dir", start_time=0.0, duration_s=61.0) fakedet = create_fake_detector(sampling_rate_hz=50.0) sim.create_observations(detectors=[fakedet], num_of_obs_per_detector=1, split_list_over_processes=False) assert len(sim.observations) == 1 obs = sim.observations[0] sstr = lbs.SpinningScanningStrategy(spin_sun_angle_rad=0.0, precession_rate_hz=0.0, spin_rate_hz=1.0) sim.generate_spin2ecl_quaternions(scanning_strategy=sstr, delta_time_s=0.5) instr = lbs.InstrumentInfo(spin_boresight_angle_rad=np.deg2rad(15.0)) pointings_and_polangle = lbs.get_pointings( obs, spin2ecliptic_quats=sim.spin2ecliptic_quats, detector_quats=np.array([[0.0, 0.0, 0.0, 1.0]]), bore2spin_quat=instr.bore2spin_quat, ) colatitude = pointings_and_polangle[..., 0] reference_spin2ecliptic_file = Path( __file__).parent / "reference_spin2ecl.txt.gz" reference = np.loadtxt(reference_spin2ecliptic_file) assert np.allclose(sim.spin2ecliptic_quats.quats, reference) reference_pointings_file = Path( __file__).parent / "reference_pointings.txt.gz" reference = np.loadtxt(reference_pointings_file) assert np.allclose(pointings_and_polangle[0, :, :], reference) # Check that the colatitude does not depart more than ±15° from # the Ecliptic assert np.allclose(np.rad2deg(np.max(colatitude)), 90 + 15, atol=0.01) assert np.allclose(np.rad2deg(np.min(colatitude)), 90 - 15, atol=0.01) sim.flush()
def test_write_hdf5_mpi(tmp_path): start_time = 0 time_span_s = 60 sampling_hz = 10 sim = lbs.Simulation( base_path=tmp_path, start_time=start_time, duration_s=time_span_s, ) det = lbs.DetectorInfo( name="Dummy detector", sampling_rate_hz=sampling_hz, bandcenter_ghz=100.0, quat=[0.0, 0.0, 0.0, 1.0], ) num_of_obs = 12 sim.create_observations(detectors=[det], num_of_obs_per_detector=num_of_obs) file_names = lbs.write_observations( sim, subdir_name="tod", file_name_mask="litebird_tod{global_index:04d}.h5") assert len(file_names) == len(sim.observations) if lbs.MPI_ENABLED: # Wait that all the processes have completed writing the files lbs.MPI_COMM_WORLD.barrier() tod_path = sim.base_path / "tod" files_found = list(tod_path.glob("litebird_tod*.h5")) assert len(files_found) == num_of_obs, ( f"{len(files_found)} files found in {tod_path} instead of " + f"{num_of_obs}: {files_found}") for idx in range(num_of_obs): cur_tod = tod_path / f"litebird_tod{idx:04d}.h5" assert cur_tod.is_file(), f"File {cur_tod} was expected but not found"
def test_scan_map(): # The purpose of this test is to simulate the motion of the spacecraft # for one year (see `time_span_s`) and produce *two* maps: the first # is associated with the Observation `obs1` and is built using # `scan_map_in_observations` and `make_bin_map`, the second is associated # the Observation `obs2` and is built directly filling in the test # `tod`, `psi` and `pixind` and then using `make_bin_map` # In the final test `out_map1` is compared with both `out_map2` and the # input map. Both simulations use two orthogonal detectors at the boresight # and input maps generated with `np.random.normal`. start_time = 0 time_span_s = 365 * 24 * 3600 nside = 16 sampling_hz = 1 hwp_radpsec = 4.084_070_449_666_731 npix = hp.nside2npix(nside) sim = lbs.Simulation(start_time=start_time, duration_s=time_span_s) scanning = lbs.SpinningScanningStrategy( spin_sun_angle_rad=0.785_398_163_397_448_3, precession_rate_hz=8.664_850_513_998_931e-05, spin_rate_hz=0.000_833_333_333_333_333_4, start_time=start_time, ) spin2ecliptic_quats = scanning.generate_spin2ecl_quaternions( start_time, time_span_s, delta_time_s=7200) instr = lbs.InstrumentInfo( boresight_rotangle_rad=0.0, spin_boresight_angle_rad=0.872_664_625_997_164_8, spin_rotangle_rad=3.141_592_653_589_793, ) detT = lbs.DetectorInfo( name="Boresight_detector_T", sampling_rate_hz=sampling_hz, bandcenter_ghz=100.0, quat=[0.0, 0.0, 0.0, 1.0], ) detB = lbs.DetectorInfo( name="Boresight_detector_B", sampling_rate_hz=sampling_hz, bandcenter_ghz=100.0, quat=[0.0, 0.0, 1.0 / np.sqrt(2.0), 1.0 / np.sqrt(2.0)], ) np.random.seed(seed=123_456_789) maps = np.random.normal(0, 1, (3, npix)) in_map = {"Boresight_detector_T": maps, "Boresight_detector_B": maps} (obs1, ) = sim.create_observations(detectors=[detT, detB]) (obs2, ) = sim.create_observations(detectors=[detT, detB]) pointings = lbs.scanning.get_pointings( obs1, spin2ecliptic_quats=spin2ecliptic_quats, detector_quats=[detT.quat, detB.quat], bore2spin_quat=instr.bore2spin_quat, ) lbs.scan_map_in_observations(obs1, pointings, hwp_radpsec, in_map, fill_psi_and_pixind_in_obs=True) out_map1 = lbs.make_bin_map(obs1, nside).T times = obs2.get_times() obs2.pixind = np.empty(obs2.tod.shape, dtype=np.int32) obs2.psi = np.empty(obs2.tod.shape) for idet in range(obs2.n_detectors): obs2.pixind[idet, :] = hp.ang2pix(nside, pointings[idet, :, 0], pointings[idet, :, 1]) obs2.psi[idet, :] = np.mod( pointings[idet, :, 2] + 2 * times * hwp_radpsec, 2 * np.pi) for idet in range(obs2.n_detectors): obs2.tod[idet, :] = ( maps[0, obs2.pixind[idet, :]] + np.cos(2 * obs2.psi[idet, :]) * maps[1, obs2.pixind[idet, :]] + np.sin(2 * obs2.psi[idet, :]) * maps[2, obs2.pixind[idet, :]]) out_map2 = lbs.make_bin_map(obs2, nside).T np.testing.assert_allclose(out_map1, in_map["Boresight_detector_T"], rtol=1e-6, atol=1e-6) np.testing.assert_allclose(out_map1, out_map2, rtol=1e-6, atol=1e-6)
def test_destriper(tmp_path): sim = lbs.Simulation(base_path=tmp_path / "destriper_output", start_time=0, duration_s=86400.0) sim.generate_spin2ecl_quaternions( scanning_strategy=lbs.SpinningScanningStrategy( spin_sun_angle_rad=np.deg2rad(30), # CORE-specific parameter spin_rate_hz=0.5 / 60, # Ditto # We use astropy to convert the period (4 days) in # seconds precession_rate_hz=1.0 / (4 * u.day).to("s").value, )) instr = lbs.InstrumentInfo(name="core", spin_boresight_angle_rad=np.deg2rad(65)) sim.create_observations( detectors=[ lbs.DetectorInfo(name="0A", sampling_rate_hz=10), lbs.DetectorInfo(name="0B", sampling_rate_hz=10, quat=lbs.quat_rotation_z(np.pi / 2)), ], # num_of_obs_per_detector=lbs.MPI_COMM_WORLD.size, dtype_tod=np.float64, n_blocks_time=lbs.MPI_COMM_WORLD.size, split_list_over_processes=False, ) # Generate some white noise rs = RandomState(MT19937(SeedSequence(123456789))) for curobs in sim.observations: curobs.tod *= 0.0 curobs.tod += rs.randn(*curobs.tod.shape) params = lbs.DestriperParameters( nside=16, nnz=3, baseline_length=100, iter_max=10, return_hit_map=True, return_binned_map=True, return_destriped_map=True, return_npp=True, return_invnpp=True, return_rcond=True, ) results = lbs.destripe(sim, instr, params=params) ref_map_path = Path(__file__).parent / "destriper_reference" hit_map_filename = ref_map_path / "destriper_hit_map.fits.gz" # healpy.write_map(hit_map_filename, results.hit_map, dtype="int32", overwrite=True) np.testing.assert_allclose( results.hit_map, healpy.read_map(hit_map_filename, field=None, dtype=np.int32)) binned_map_filename = ref_map_path / "destriper_binned_map.fits.gz" # healpy.write_map( # binned_map_filename, # results.binned_map, # dtype=list((np.float32 for i in range(3))), # overwrite=True, # ) ref_binned = healpy.read_map(binned_map_filename, field=None, dtype=list((np.float32 for i in range(3)))) assert results.binned_map.shape == ref_binned.shape np.testing.assert_allclose(results.binned_map, ref_binned, rtol=1e-2, atol=1e-3) destriped_map_filename = ref_map_path / "destriper_destriped_map.fits.gz" # healpy.write_map( # destriped_map_filename, # results.destriped_map, # dtype=list((np.float32 for i in range(3))), # overwrite=True, # ) ref_destriped = healpy.read_map(destriped_map_filename, field=None, dtype=list((np.float32 for i in range(3)))) assert results.destriped_map.shape == ref_destriped.shape np.testing.assert_allclose(results.destriped_map, ref_destriped, rtol=1e-2, atol=1e-3) npp_filename = ref_map_path / "destriper_npp.fits.gz" # healpy.write_map( # npp_filename, # results.npp, # dtype=list((np.float32 for i in range(6))), # overwrite=True, # ) ref_npp = healpy.read_map(npp_filename, field=None, dtype=list((np.float32 for i in range(6)))) assert results.npp.shape == ref_npp.shape np.testing.assert_allclose(results.npp, ref_npp, rtol=1e-2, atol=1e-3) invnpp_filename = ref_map_path / "destriper_invnpp.fits.gz" # healpy.write_map( # invnpp_filename, # results.invnpp, # dtype=list((np.float32 for i in range(6))), # overwrite=True, # ) ref_invnpp = healpy.read_map(invnpp_filename, field=None, dtype=list((np.float32 for i in range(6)))) assert results.invnpp.shape == ref_invnpp.shape np.testing.assert_allclose(results.invnpp, ref_invnpp, rtol=1e-2, atol=1e-3) rcond_filename = ref_map_path / "destriper_rcond.fits.gz" # healpy.write_map( # rcond_filename, # results.rcond, # dtype=np.float32, # overwrite=True, # ) assert np.allclose( results.rcond, healpy.read_map(rcond_filename, field=None, dtype=np.float32))
def main(data_path: Path): sim = lbs.Simulation( parameter_file=sys.argv[1], name="In-flight estimation of the beam properties", description=""" This report contains the result of a simulation of the reconstruction of in-flight beam parameters, assuming a scanning strategy and some noise/optical properties of a detector. """, ) params = load_parameters(sim) det = read_detector(sim.parameters["detector"], sim.imo) # TODO: This should be done by the framework copyfile(src=params.sed_file_name, dst=sim.base_path / params.sed_file_name.name) # Calculate the brightness temperature of the planet over the band sed_data = np.loadtxt(params.sed_file_name, delimiter=",") sed_fn = interpolate.interp1d(sed_data[:, 0], sed_data[:, 1]) planet_temperature_k = ( integrate.quad( sed_fn, params.detector.bandcenter_ghz - params.detector.bandwidth_ghz / 2, params.detector.bandcenter_ghz + params.detector.bandwidth_ghz / 2, )[0] / params.detector.bandwidth_ghz ) beam_solid_angle = calc_beam_solid_angle( fwhm_arcmin=params.fwhm_arcmin, ecc=params.ecc ) sampling_time_s = 1.0 / params.detector.sampling_rate_hz input_map_file_name = params.scanning_simulation / "map.fits.gz" hit_map, time_map_s, dist_map_m2 = healpy.read_map( input_map_file_name, field=(0, 1, 2), verbose=False, dtype=np.float32 ) nside = healpy.npix2nside(len(dist_map_m2)) pixel_theta, pixel_phi = healpy.pix2ang(nside, np.arange(len(hit_map))) gamma_map = beamfunc( healpy.pix2ang(nside, np.arange(len(hit_map))), params.fwhm_arcmin, params.amplitude, params.ecc, np.deg2rad(params.incl_deg), np.deg2rad(params.beam_deg[0]), np.deg2rad(params.beam_deg[1]), ) mask = (hit_map > 0.0) & (gamma_map > params.amplitude * np.exp(-np.log(2) * 9 / 2)) assert hit_map[mask].size > 0, "no data available for the fit" sim.append_to_report( r""" ## Detector properties Parameter | Value --------- | ----------------- Channel | {{det.channel}} Sampling time | {{ "%.3f"|format(sampling_time_s) }} s NET | {{det.net_ukrts}} μK·√s Bandwidth | {{det.bandwidth_ghz}} GHz Band center | {{det.bandcenter_ghz}} GHz FWHM | {{det.fwhm_arcmin}} arcmin Beam solid angle | {{ "%.3e"|format(beam_solid_angle)}} sterad Amplitude | {{ "%.3e"|format(params.amplitude) }} ## Properties of the planet Parameter | Value --------- | ---------------- Brightness temperature | {{ "%.1f"|format(planet_temperature_k) }} K Effective radius | {{ "%.0e"|format(params.planet_radius_m) }} m ## Beam scanning Parameter | Value --------- | ---------------- Pixels used in the fit | {{ num_of_pixels_used }} Integration time | {{ integration_time_s }} s """, det=params.detector, params=params, beam_solid_angle=beam_solid_angle, sampling_time_s=sampling_time_s, planet_temperature_k=planet_temperature_k, num_of_pixels_used=len(mask[mask]), integration_time_s=np.sum(time_map_s[mask]), ) error_amplitude_map = ( beam_solid_angle * (params.detector.net_ukrts * 1e-6) / ( np.pi * (params.planet_radius_m ** 2) * planet_temperature_k * np.sqrt(sampling_time_s) ) ) * dist_map_m2 ( plot_size_deg, gamma_fig, gamma_error_fig, gamma_over_error_fig, ) = create_gamma_plots( gamma_map, error_amplitude_map, nside, fwhm_arcmin=params.detector.fwhm_arcmin, theta_beam=np.deg2rad(params.beam_deg[0]), phi_beam=np.deg2rad(params.beam_deg[1]), ) sim.append_to_report( r""" ## Error on beam estimation This is a representation of the model of the far sidelobe beam used in the simulation, in $`(u,v)`$ coordinates, where $`u = \sin\theta\,\cos\phi`$ and $`v = \sin\theta\sin\phi`$: ![](gamma.svg) Here is the result of the estimate of $`\delta\gamma`$, using the following formula: ```math \delta\gamma(\vec r) = \frac{\Omega_b \cdot \text{WN}}{\pi r_\text{pl}^2\,T_\text{br,pl}\,\sqrt\tau} \sqrt{\frac1{\sum_{i=1}^N \left(\frac1{4\pi d_\text{pl}^2(t_i)}\right)^2}}, ``` where $`N`$ is the number of samples observed along direction $`\vec r`$, WN is the white noise level expressed as $`\text{K}\cdot\sqrt{s}`$, $`r_\text{pl}`$ is the planet's radius, $`d_\text{pl}(t)`$ is the planet-spacecraft distance at time $`t`$, and $`\tau`$ is the sample integration time (assumed equal for all the samples). And here is a plot of $`\delta\gamma`$, again in $`(u, v)`$ coordinates: ![](gamma_error.svg) The ratio $`\gamma / \delta\gamma`$ represents the S/N ratio: ![](gamma_over_error.svg) The size of each plot is {{ "%.1f"|format(plot_size_deg) }}°, and the white shadow represents the size of the FWHM ({{ "%.1f"|format(fwhm_arcmin) }} arcmin). """, fwhm_arcmin=params.detector.fwhm_arcmin, plot_size_deg=plot_size_deg, figures=[ (gamma_fig, "gamma.svg"), (gamma_error_fig, "gamma_error.svg"), (gamma_over_error_fig, "gamma_over_error.svg"), ], ) destfile = sim.base_path / params.error_amplitude_map_file_name healpy.write_map( destfile, [gamma_map, error_amplitude_map], coord="DETECTOR", column_names=["GAMMA", "ERR"], column_units=["", ""], dtype=[np.float32, np.float32, np.float32], overwrite=True, ) fwhm_estimates_arcmin = np.empty(params.num_of_mc_runs) ampl_estimates = np.empty(len(fwhm_estimates_arcmin)) ecc_estimates = np.empty(len(fwhm_estimates_arcmin)) incl_estimates = np.empty(len(fwhm_estimates_arcmin)) theta_beam_estimates = np.empty(len(fwhm_estimates_arcmin)) phi_beam_estimates = np.empty(len(fwhm_estimates_arcmin)) for i in tqdm(range(len(fwhm_estimates_arcmin))): noise_gamma_map = gamma_map + error_amplitude_map * np.random.randn( len(dist_map_m2) ) # Run the fit best_fit, pcov = optimize.curve_fit( beamfunc, np.vstack((pixel_theta[mask], pixel_phi[mask])), noise_gamma_map[mask], p0=[ params.fwhm_arcmin, params.amplitude, params.ecc, np.deg2rad(params.incl_deg), np.deg2rad(params.beam_deg[0]), np.deg2rad(params.beam_deg[1]), ], maxfev=5000, ) fwhm_estimates_arcmin[i] = best_fit[0] ampl_estimates[i] = best_fit[1] ecc_estimates[i] = best_fit[2] incl_estimates[i] = best_fit[3] theta_beam_estimates[i] = best_fit[4] phi_beam_estimates[i] = best_fit[5] figs = [] for (dataset, name) in [ (fwhm_estimates_arcmin, "FWHM [arcmin]"), (ampl_estimates, "AMPL [arcmin]"), (ecc_estimates, "ECC"), (incl_estimates, "INCL [rad]"), (theta_beam_estimates, "THETA [rad]"), (phi_beam_estimates, "PHI [rad]"), ]: fig, ax = plt.subplots() ax.hist(dataset) ax.set_xlabel(name) ax.set_ylabel("Counts") figs.append(fig) sim.append_to_report( """ ## Results of the Monte Carlo simulation Parameter | Value ---------- | ----------------- # of runs | {{ num_of_runs }} FWHM | {{"%.3f"|format(fwhm_arcmin)}} ± {{"%.3e"|format(fwhm_err)}} arcmin γ0 | {{"%.3f"|format(ampl)}} ± {{"%.3e"|format(ampl_err)}} arcmin Ecc | {{"%.3f"|format(ecc)}} ± {{"%.3e"|format(ecc_err)}} Inclination | {{"%.3f"|format(incl)}} ± {{"%.3e"|format(incl_err)}} rad Theta_beam | {{"%.3f"|format(theta)}} ± {{"%.3e"|format(theta_err)}} rad Phi_beam | {{"%.3f"|format(phi)}} ± {{"%.3e"|format(phi_err)}} rad ![](fwhm_distribution.svg) ![](ampl_distribution.svg) ![](ecc_distribution.svg) ![](incl_distribution.svg) ![](theta_distribution.svg) ![](phi_distribution.svg) """, figures=[ (figs[0], "fwhm_distribution.svg"), (figs[1], "ampl_distribution.svg"), (figs[2], "ecc_distribution.svg"), (figs[3], "incl_distribution.svg"), (figs[4], "theta_distribution.svg"), (figs[5], "phi_distribution.svg"), ], num_of_runs=len(fwhm_estimates_arcmin), fwhm_arcmin=np.mean(fwhm_estimates_arcmin), fwhm_err=np.std(fwhm_estimates_arcmin), ampl=np.mean(ampl_estimates), ampl_err=np.std(ampl_estimates), ecc=np.mean(ecc_estimates), ecc_err=np.std(ampl_estimates), incl=np.mean(incl_estimates), incl_err=np.std(incl_estimates), theta=np.mean(theta_beam_estimates), theta_err=np.std(theta_beam_estimates), phi=np.mean(phi_beam_estimates), phi_err=np.std(phi_beam_estimates), ) json_file_name = sim.base_path / "results.json" with json_file_name.open("w") as f: json.dump( { "mc_fwhm_arcmin": fwhm_estimates_arcmin.tolist(), "mc_fwhm_arcmin_mean": np.mean(fwhm_estimates_arcmin), "mc_fwhm_err": np.std(fwhm_estimates_arcmin), "mc_amplitudes": ampl_estimates.tolist(), "mc_amplitudes_mean": np.mean(ampl_estimates), "mc_amplitudes_err": np.std(ampl_estimates), "mc_eccentricity": ecc_estimates.tolist(), "mc_eccentricity_mean": np.mean(ecc_estimates), "mc_eccentricity_err": np.std(ampl_estimates), "mc_inclinations": incl_estimates.tolist(), "mc_inclinations_mean": np.mean(incl_estimates), "mc_inclinations_err": np.std(incl_estimates), "mc_theta_beam": theta_beam_estimates.tolist(), "mc_theta_mean": np.mean(theta_beam_estimates), "mc_theta_err": np.std(theta_beam_estimates), "mc_phi_beam": phi_beam_estimates.tolist(), "mc_phi_mean": np.mean(phi_beam_estimates), "mc_phi_err": np.std(phi_beam_estimates), }, f, ) sim.flush()
import numpy as np import astropy.units as u from numpy.random import MT19937, RandomState, SeedSequence import litebird_sim as lbs sim = lbs.Simulation(base_path="/tmp/destriper_output", start_time=0, duration_s=86400.0) sim.generate_spin2ecl_quaternions( scanning_strategy=lbs.SpinningScanningStrategy( spin_sun_angle_rad=np.deg2rad(30), # CORE-specific parameter spin_rate_hz=0.5 / 60, # Ditto # We use astropy to convert the period (4 days) in # seconds precession_rate_hz=1.0 / (4 * u.day).to("s").value, )) instr = lbs.InstrumentInfo(name="core", spin_boresight_angle_rad=np.deg2rad(65)) # We create two detectors, whose polarization angles are separated by π/2 sim.create_observations( detectors=[ lbs.DetectorInfo(name="0A", sampling_rate_hz=10), lbs.DetectorInfo(name="0B", sampling_rate_hz=10, quat=lbs.quat_rotation_z(np.pi / 2)), ], dtype_tod=np.float64, n_blocks_time=lbs.MPI_COMM_WORLD.size,
def test_solar_dipole_fit(tmpdir): tmpdir = Path(tmpdir) test = unittest.TestCase() # The purpose of this test is to simulate the motion of the spacecraft # for one year (see `time_span_s`) and produce *two* timelines: the first # is associated with variables `*_s_o` and refers to the case of a nonzero # velocity of the Solar System, and the second is associated with variables # `*_o` and assumes that the reference frame of the Solar System is the # same as the CMB's (so that there is no dipole). start_time = Time("2022-01-01") time_span_s = 365 * 24 * 3600 nside = 256 sampling_hz = 1 sim = lbs.Simulation(start_time=start_time, duration_s=time_span_s) scanning = lbs.SpinningScanningStrategy( spin_sun_angle_rad=0.785_398_163_397_448_3, precession_rate_hz=8.664_850_513_998_931e-05, spin_rate_hz=0.000_833_333_333_333_333_4, start_time=start_time, ) spin2ecliptic_quats = scanning.generate_spin2ecl_quaternions( start_time, time_span_s, delta_time_s=7200 ) instr = lbs.InstrumentInfo( boresight_rotangle_rad=0.0, spin_boresight_angle_rad=0.872_664_625_997_164_8, spin_rotangle_rad=3.141_592_653_589_793, ) det = lbs.DetectorInfo( name="Boresight_detector", sampling_rate_hz=sampling_hz, bandcenter_ghz=100.0, quat=[0.0, 0.0, 0.0, 1.0], ) (obs_s_o,) = sim.create_observations(detectors=[det]) (obs_o,) = sim.create_observations(detectors=[det]) pointings = lbs.scanning.get_pointings( obs_s_o, spin2ecliptic_quats=spin2ecliptic_quats, detector_quats=[det.quat], bore2spin_quat=instr.bore2spin_quat, ) orbit_s_o = lbs.SpacecraftOrbit(obs_s_o.start_time) orbit_o = lbs.SpacecraftOrbit(obs_o.start_time, solar_velocity_km_s=0.0) assert orbit_s_o.solar_velocity_km_s == 369.8160 assert orbit_o.solar_velocity_km_s == 0.0 pos_vel_s_o = lbs.spacecraft_pos_and_vel(orbit_s_o, obs_s_o, delta_time_s=86400.0) pos_vel_o = lbs.spacecraft_pos_and_vel(orbit_o, obs_o, delta_time_s=86400.0) assert pos_vel_s_o.velocities_km_s.shape == (366, 3) assert pos_vel_o.velocities_km_s.shape == (366, 3) lbs.add_dipole_to_observations( obs_s_o, pointings, pos_vel_s_o, dipole_type=lbs.DipoleType.LINEAR ) lbs.add_dipole_to_observations( obs_o, pointings, pos_vel_o, dipole_type=lbs.DipoleType.LINEAR ) npix = hp.nside2npix(nside) pix_indexes = hp.ang2pix(nside, pointings[0, :, 0], pointings[0, :, 1]) h = np.zeros(npix) m = np.zeros(npix) map_s_o = np.zeros(npix) map_o = np.zeros(npix) bin_map( tod=obs_s_o.tod, pixel_indexes=pix_indexes, binned_map=map_s_o, accum_map=m, hit_map=h, ) import healpy healpy.write_map(tmpdir / "map_s_o.fits.gz", map_s_o, overwrite=True) bin_map( tod=obs_o.tod, pixel_indexes=pix_indexes, binned_map=map_o, accum_map=m, hit_map=h, ) healpy.write_map(tmpdir / "map_o.fits.gz", map_o, overwrite=True) dip_map = map_s_o - map_o assert np.abs(np.nanmean(map_s_o) * 1e6) < 1 assert np.abs(np.nanmean(map_o) * 1e6) < 1 assert np.abs(np.nanmean(dip_map) * 1e6) < 1 dip_map[np.isnan(dip_map)] = healpy.UNSEEN mono, dip = hp.fit_dipole(dip_map) r = hp.Rotator(coord=["E", "G"]) l, b = hp.vec2ang(r(dip), lonlat=True) # Amplitude, longitude and latitude test.assertAlmostEqual(np.sqrt(np.sum(dip ** 2)) * 1e6, 3362.08, 1) test.assertAlmostEqual(l[0], 264.021, 1) test.assertAlmostEqual(b[0], 48.253, 1)
def main(): warnings.filterwarnings("ignore", category=ErfaWarning) sim = lbs.Simulation( parameter_file=sys.argv[1], name="Observation of planets", description=""" This report contains the result of a simulation of the observation of the sky, particularly with respect to the observation of planets. """, ) params = load_parameters(sim) if lbs.MPI_ENABLED: log.info("Using MPI with %d processes", lbs.MPI_COMM_WORLD.size) else: log.info("Not using MPI, serial execution") log.info("Generating the quaternions") scanning_strategy = read_scanning_strategy( sim.parameters["scanning_strategy"], sim.imo, sim.start_time) sim.generate_spin2ecl_quaternions( scanning_strategy=scanning_strategy, delta_time_s=params.spin2ecl_delta_time_s) log.info("Creating the observations") instr = lbs.InstrumentInfo( name="instrum", spin_boresight_angle_rad=params.spin_boresight_angle_rad) detector = lbs.DetectorInfo( sampling_rate_hz=params.detector_sampling_rate_hz) conversions = [ ("years", astropy.units.year), ("year", astropy.units.year), ("days", astropy.units.day), ("day", astropy.units.day), ("hours", astropy.units.hour), ("hour", astropy.units.hour), ("minutes", astropy.units.minute), ("min", astropy.units.minute), ("sec", astropy.units.second), ("s", astropy.units.second), ("km", astropy.units.kilometer), ("Km", astropy.units.kilometer), ("au", astropy.units.au), ("AU", astropy.units.au), ("deg", astropy.units.deg), ("rad", astropy.units.rad), ] def conversion(x, new_unit): if isinstance(x, str): for conv_str, conv_unit in conversions: if x.endswith(" " + conv_str): value = float(x.replace(conv_str, "")) return (value * conv_unit).to(new_unit).value break else: return float(x) sim_params = sim.parameters["simulation"] durations = ["duration_s", "duration_of_obs_s"] for dur in durations: sim_params[dur] = conversion(sim_params[dur], "s") delta_t_s = sim_params["duration_of_obs_s"] sim.create_observations( detectors=[detector], num_of_obs_per_detector=int(sim_params["duration_s"] / sim_params["duration_of_obs_s"]), ) ################################################################# # Here begins the juicy part log.info("The loop starts on %d processes", lbs.MPI_COMM_WORLD.size) sky_hitmap = np.zeros(healpy.nside2npix(params.output_nside), dtype=np.int32) detector_hitmap = np.zeros(healpy.nside2npix(params.output_nside), dtype=np.int32) dist_map_m2 = np.zeros(len(detector_hitmap)) iterator = tqdm if lbs.MPI_ENABLED and lbs.MPI_COMM_WORLD.rank != 0: iterator = lambda x: x # Time variable inizialized at the beginning of the simulation t = 0.0 for obs in iterator(sim.observations): solar_system_ephemeris.set("builtin") times = obs.get_times(astropy_times=True) # We only compute the planet's position for the first sample in # the observation and then assume that it does not move # significantly. (In Ecliptic coordinates, Jupiter moves by # fractions of an arcmin over a time span of one hour) time0 = times[0] icrs_pos = get_ecliptic_vec( get_body_barycentric(params.planet_name, time0)) earth_pos = get_ecliptic_vec(get_body_barycentric("earth", time0)) # Move the spacecraft to L2 L2_pos = earth_pos * ( 1.0 + params.earth_L2_distance_km / norm(earth_pos).to("km").value) # Creating a Lissajous orbit R1 = conversion(params.radius_au[0], "au") R2 = conversion(params.radius_au[1], "au") phi1_t = params.L2_orbital_velocity_rad_s[0] * t phi2_t = params.L2_orbital_velocity_rad_s[1] * t phase = conversion(params.phase_rad, "rad") orbit_pos = np.array([ -R1 * np.sin(np.arctan(L2_pos[1] / L2_pos[0])) * np.cos(phi1_t), R1 * np.cos(np.arctan(L2_pos[1] / L2_pos[0])) * np.cos(phi1_t), R2 * np.sin(phi2_t + phase), ]) orbit_pos = astropy.units.Quantity(orbit_pos, unit="AU") # Move the spacecraft to a Lissajous orbit around L2 sat_pos = orbit_pos + L2_pos # Compute the distance between the spacecraft and the planet distance_m = norm(sat_pos - icrs_pos).to("m").value # This is the direction of the solar system body with respect # to the spacecraft, in Ecliptic coordinates ecl_vec = (icrs_pos - sat_pos).value # The variable ecl_vec is a 3-element vector. We normalize it so # that it has length one (using the L_2 norm, hence ord=2) ecl_vec /= np.linalg.norm(ecl_vec, axis=0, ord=2) # Convert the matrix to a N×3 shape by repeating the vector: # planets move slowly, so we assume that the planet stays fixed # during this observation. ecl_vec = np.repeat(ecl_vec.reshape(1, 3), len(times), axis=0) # Calculate the quaternions that convert the Ecliptic # reference system into the detector's reference system quats = lbs.get_ecl2det_quaternions( obs, sim.spin2ecliptic_quats, detector_quats=[detector.quat], bore2spin_quat=instr.bore2spin_quat, ) # Make room for the xyz vectors in the detector's reference frame det_vec = np.empty_like(ecl_vec) # Do the rotation! lbs.all_rotate_vectors(det_vec, quats[0], ecl_vec) pixidx = healpy.vec2pix(params.output_nside, det_vec[:, 0], det_vec[:, 1], det_vec[:, 2]) bincount = np.bincount(pixidx, minlength=len(detector_hitmap)) detector_hitmap += bincount dist_map_m2 += bincount / ((4 * np.pi * (distance_m**2))**2) pointings = lbs.get_pointings(obs, sim.spin2ecliptic_quats, [detector.quat], instr.bore2spin_quat)[0] pixidx = healpy.ang2pix(params.output_nside, pointings[:, 0], pointings[:, 1]) bincount = np.bincount(pixidx, minlength=len(sky_hitmap)) sky_hitmap += bincount # updating the time variable t += delta_t_s if lbs.MPI_ENABLED: sky_hitmap = lbs.MPI_COMM_WORLD.allreduce(sky_hitmap) detector_hitmap = lbs.MPI_COMM_WORLD.allreduce(detector_hitmap) dist_map_m2 = lbs.MPI_COMM_WORLD.allreduce(dist_map_m2) time_map_s = detector_hitmap / params.detector_sampling_rate_hz dist_map_m2[dist_map_m2 > 0] = np.power(dist_map_m2[dist_map_m2 > 0], -0.5) obs_time_per_radius_s = [ time_per_radius(time_map_s, angular_radius_rad=np.deg2rad(radius_deg)) for radius_deg in params.radii_deg ] if lbs.MPI_COMM_WORLD.rank == 0: # Create a plot of the observation time of the planet as a # function of the angular radius fig, ax = plt.subplots() ax.loglog(params.radii_deg, obs_time_per_radius_s) ax.set_xlabel("Radius [deg]") ax.set_ylabel("Observation time [s]") # Create a map showing how the observation time is distributed on # the sphere (in the reference frame of the detector) healpy.orthview(time_map_s, title="Time spent observing the source", unit="s") if scanning_strategy.spin_rate_hz != 0: spin_period_min = 1.0 / (60.0 * scanning_strategy.spin_rate_hz) else: spin_period_min = 0.0 if scanning_strategy.precession_rate_hz != 0: precession_period_min = 1.0 / ( 60.0 * scanning_strategy.precession_rate_hz) else: precession_period_min = 0.0 sim.append_to_report( """ ## Scanning strategy parameters Parameter | Value --------- | -------------- Angle between the spin axis and the Sun-Earth axis | {{ sun_earth_angle_deg }} deg Angle between the spin axis and the boresight | {{ spin_boresight_angle_deg }} deg Precession period | {{ precession_period_min }} min Spin period | {{ spin_period_min }} min ## Observation of {{ params.planet_name | capitalize }} ![](detector_hitmap.png) The overall time spent in the map is {{ overall_time_s }} seconds. The time resolution of the simulation was {{ delta_time_s }} seconds. Angular radius [deg] | Time spent [s] -------------------- | ------------------------ {% for row in radius_vs_time_s -%} {{ "%.1f"|format(row[0]) }} | {{ "%.1f"|format(row[1]) }} {% endfor -%} ![](radius_vs_time.svg) """, figures=[(plt.gcf(), "detector_hitmap.png"), (fig, "radius_vs_time.svg")], params=params, overall_time_s=np.sum(detector_hitmap) / params.detector_sampling_rate_hz, radius_vs_time_s=list(zip(params.radii_deg, obs_time_per_radius_s)), delta_time_s=1.0 / params.detector_sampling_rate_hz, sun_earth_angle_deg=np.rad2deg( scanning_strategy.spin_sun_angle_rad), spin_boresight_angle_deg=np.rad2deg( params.spin_boresight_angle_rad), precession_period_min=precession_period_min, spin_period_min=spin_period_min, det=detector, ) healpy.write_map( params.output_map_file_name, (detector_hitmap, time_map_s, dist_map_m2, sky_hitmap), coord="DETECTOR", column_names=["HITS", "OBSTIME", "SQDIST", "SKYHITS"], column_units=["", "s", "m^2", ""], dtype=[np.int32, np.float32, np.float64, np.int32], overwrite=True, ) np.savetxt( params.output_table_file_name, np.array(list(zip(params.radii_deg, obs_time_per_radius_s))), fmt=["%.2f", "%.5e"], ) with (sim.base_path / "parameters.json").open("wt") as outf: time_value = scanning_strategy.start_time if not isinstance(time_value, (int, float)): time_value = str(time_value) json.dump( { "scanning_strategy": { "spin_sun_angle_rad": scanning_strategy.spin_sun_angle_rad, "precession_rate_hz": scanning_strategy.precession_rate_hz, "spin_rate_hz": scanning_strategy.spin_rate_hz, "start_time": time_value, }, "detector": { "sampling_rate_hz": params.detector_sampling_rate_hz }, "planet": { "name": params.planet_name }, }, outf, indent=2, ) sim.flush()
from astropy.time import Time import numpy as np import litebird_sim as lbs import matplotlib.pylab as plt start_time = Time("2022-01-01") time_span_s = 120.0 # Two minutes sampling_hz = 20 sim = lbs.Simulation(start_time=start_time, duration_s=time_span_s) # We pick a simple scanning strategy where the spin axis is aligned # with the Sun-Earth axis, and the spacecraft spins once every minute scanning = lbs.SpinningScanningStrategy( spin_sun_angle_rad=np.deg2rad(0), precession_rate_hz=0, spin_rate_hz=1 / 60, start_time=start_time, ) # The spacecraft spins pretty fast (once per minute!), so we # need to pick delta_time_s ≪ 1/spin_rate_hz spin2ecliptic_quats = scanning.generate_spin2ecl_quaternions(start_time, time_span_s, delta_time_s=5.0) # We simulate an instrument whose boresight is perpendicular to # the spin axis. instr = lbs.InstrumentInfo( boresight_rotangle_rad=0.0, spin_boresight_angle_rad=np.deg2rad(90),