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_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_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_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, 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) # 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 get_pointings 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] 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_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 __init__(self, spin2ecliptic_quats, obs, bore2spin_quat, nside): self.obs = obs self.keydict = {"timestamps": obs.get_times()} nsamples = len(self.keydict["timestamps"]) self.keydict["flags"] = np.zeros(nsamples, dtype="uint8") pointings = lbs.get_pointings( obs=obs, spin2ecliptic_quats=spin2ecliptic_quats, detector_quats=obs.quat, bore2spin_quat=bore2spin_quat, ) healpix_base = healpix.Healpix_Base(nside=nside, scheme="NEST") for (i, det) in enumerate(obs.name): if pointings[i].dtype == np.float64: curpnt = pointings[i] else: logging.warning( "converting pointings for %s from %s to float64", obs.name[i], str(pointings[i].dtype), ) curpnt = np.array(pointings[i], dtype=np.float64) if obs.tod[i].dtype == np.float64: self.keydict[f"signal_{det}"] = obs.tod[i] else: logging.warning( "converting TODs for %s from %s to float64", obs.name[i], str(pointings[i].dtype), ) self.keydict[f"signal_{det}"] = np.array(obs.tod[i], dtype=np.float64) self.keydict[f"pixels_{det}"] = healpix_base.ang2pix(curpnt[:, 0:2]) self.keydict[f"weights_{det}"] = np.stack( (np.ones(nsamples), np.cos(2 * curpnt[:, 2]), np.sin(2 * curpnt[:, 2])) ).transpose()
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()