diff --git a/docs/source/user_manual/search_space.rst b/docs/source/user_manual/search_space.rst index a39b139b1..4c14d96d9 100644 --- a/docs/source/user_manual/search_space.rst +++ b/docs/source/user_manual/search_space.rst @@ -78,8 +78,8 @@ The ``VelocityGridSearch`` strategy searches a uniform grid of x and y velocitie | ``max_vy`` | The maximum velocity in the y-dimension (pixels per day). | +------------------------+-----------------------------------------------------------+ -EclipticSearch --------------- +EclipticCenteredSearch +---------------------- The grid is defined by two sets of parameters: a sampling of absolute velocities in pixels per day and a sampling of the velocities' angles in degrees or radians. Each sampling consists of values defining the range and number of sampling steps. @@ -95,37 +95,32 @@ Given the linear sampling for both velocities and angles, the full set of candid where ``angles`` contains the list of angles to test and ``velocities`` contains the list of velocities. -The list of velocities is created from the given bounds (``min_vel`` and ``max_vel``) and number of steps (``vel_steps``). The range is inclusive of both bounds. +The list of velocities is created from the given bounds list ``velocities=[min_vel, max_vel, vel_steps]``. The range is inclusive of both bounds. Each angle in the list is computed as an **offset** from the ecliptic angle. KBMOD uses the following ordering for extracting the ecliptic. -1. If ``force_ecliptic`` is provided (is not ``None``) in the generator’s configuration that value is used directly. +1. If ``given_ecliptic`` is provided (is not ``None``) in the generator’s configuration that value is used directly. 2. If the first image has a WCS, the ecliptic is estimated from that WCS. 3. A default ecliptic of 0.0 is used. -The list of angles spans ``[ecliptic + min_ang_offset, ecliptic + max_ang_offset]`` inclusive of both bounds. Angles can be specified in degrees or radians (as noted by the ``angle_units`` parameter) but must be consistent among all angles. +The list of angles used is defined from the list ``angles=[min_offset, max_offset, angle_steps]`` and will span ``[ecliptic + min_offset, ecliptic + max_offset]`` inclusive of both bounds. Angles can be specified in degrees or radians (as noted by the ``angle_units`` parameter) but must be consistent among all angles. +------------------------+-----------------------------------------------------+ | **Parameter** | **Interpretation** | +------------------------+-----------------------------------------------------+ -| ``ang_steps`` | The number of angle steps. | +| ``angles`` | A length 3 tuple with the minimum angle offset, | +| | the maximum offset, and the number of angles to | +| | to search through (angles specified in either | +| | radians or degrees) | +------------------------+-----------------------------------------------------+ | ``angle_units`` | The units to use for angles. | | | Either 'degrees' or 'radians' | +------------------------+-----------------------------------------------------+ -| ``min_ang_offset `` | The minimum angular offset from the ecliptic | -| | (in either radians or degrees). | -+------------------------+-----------------------------------------------------+ -| ``max_ang_offset `` | The maximum angular offset from the ecliptic | +| ``given_ecliptic `` | The given value of the ecliptic angle | | | (in either radians or degrees). | +------------------------+-----------------------------------------------------+ -| ``force_ecliptic `` | The given value of the ecliptic angle | -| | (in either radians or degrees). | -+------------------------+-----------------------------------------------------+ -| ``vel_steps `` | The number of velocity steps. | -+------------------------+-----------------------------------------------------+ -| ``min_vel `` | The minimum velocity magnitude (in pixels per day). | -+------------------------+-----------------------------------------------------+ -| ``max_vel `` | The maximum velocity magnitude (in pixels per day). | +| ``velocities `` | A length 3 tuple with the minimum velocity (in | +| | pixels per day), the maximum velocity (in pixels | +| | per day), and number of velocities to test. | +------------------------+-----------------------------------------------------+ diff --git a/notebooks/KBMOD_Demo.ipynb b/notebooks/KBMOD_Demo.ipynb index 9507b54b1..5fa253287 100644 --- a/notebooks/KBMOD_Demo.ipynb +++ b/notebooks/KBMOD_Demo.ipynb @@ -125,13 +125,9 @@ " # Use search parameters (including a force ecliptic angle of 0.0)\n", " # to match what we know is in the demo data.\n", " \"generator_config\": {\n", - " \"name\": \"EclipticSearch\",\n", - " \"vel_steps\": 21,\n", - " \"min_vel\": 0.0,\n", - " \"max_vel\": 20.0,\n", - " \"ang_steps\": 11,\n", - " \"min_ang_offset\": -0.5,\n", - " \"max_ang_offset\": 0.5,\n", + " \"name\": \"EclipticCenteredSearch\",\n", + " \"angles\": [-0.5, 0.5, 11],\n", + " \"velocities\": [0.0, 20.0, 21],\n", " \"angle_units\": \"radians\",\n", " \"force_ecliptic\": 0.0,\n", " },\n", diff --git a/notebooks/create_fake_data.ipynb b/notebooks/create_fake_data.ipynb index 4faa3f0fc..86886acbc 100644 --- a/notebooks/create_fake_data.ipynb +++ b/notebooks/create_fake_data.ipynb @@ -150,13 +150,9 @@ "settings = {\n", " # Override the search data to match the known object.\n", " \"generator_config\": {\n", - " \"name\": \"EclipticSearch\",\n", - " \"vel_steps\": 21,\n", - " \"min_vel\": 0.0,\n", - " \"max_vel\": 20.0,\n", - " \"ang_steps\": 11,\n", - " \"min_ang_offset\": -0.5,\n", - " \"max_ang_offset\": 0.5,\n", + " \"name\": \"EclipticCenteredSearch\",\n", + " \"angles\": [-0.5, 0.5, 11],\n", + " \"velocities\": [0.0, 20.0, 21],\n", " \"angle_units\": \"radians\",\n", " \"force_ecliptic\": 0.0,\n", " },\n", diff --git a/src/kbmod/configuration.py b/src/kbmod/configuration.py index 1c66b02cb..94c951659 100644 --- a/src/kbmod/configuration.py +++ b/src/kbmod/configuration.py @@ -32,15 +32,11 @@ def __init__(self): "do_stamp_filter": True, "encode_num_bytes": -1, "generator_config": { - "name": "EclipticSearch", - "vel_steps": 257, - "min_vel": 92.0, - "max_vel": 526.0, - "ang_steps": 129, - "min_ang_offset": -math.pi / 15, - "max_ang_offset": math.pi / 15, + "name": "EclipticCenteredSearch", + "velocity": [92.0, 526.0, 257], + "angles": [-math.pi / 15, math.pi / 15, 129], "angle_units": "radians", - "force_ecliptic": None, + "given_ecliptic": None, }, "gpu_filter": False, "ind_output_files": True, diff --git a/src/kbmod/fake_data/demo_helper.py b/src/kbmod/fake_data/demo_helper.py index 5ca323cd0..5af7e4ce7 100644 --- a/src/kbmod/fake_data/demo_helper.py +++ b/src/kbmod/fake_data/demo_helper.py @@ -39,15 +39,11 @@ def make_demo_data(filename=None): settings = { # Override the search data to match the known object. "generator_config": { - "name": "EclipticSearch", - "vel_steps": 21, - "min_vel": 0.0, - "max_vel": 20.0, - "ang_steps": 11, - "min_ang_offset": -0.5, - "max_ang_offset": 0.5, + "name": "EclipticCenteredSearch", + "velocities": [0, 20.0, 21], + "angles": [-0.5, 0.5, 11], "angle_units": "radians", - "force_ecliptic": 0.0, + "given_ecliptic": 0.0, }, # Loosen the other filtering parameters. "clip_negative": True, diff --git a/src/kbmod/trajectory_generator.py b/src/kbmod/trajectory_generator.py index c162a981a..388198f57 100644 --- a/src/kbmod/trajectory_generator.py +++ b/src/kbmod/trajectory_generator.py @@ -334,25 +334,19 @@ def __init__(self, v_arr, ang_arr, average_angle=None, computed_ecliptic_angle=N super().__init__(v_arr[2], v_arr[0], v_arr[1], ang_arr[2], ang_min, ang_max, **kwargs) -class EclipticSearch(TrajectoryGenerator): +class EclipticCenteredSearch(TrajectoryGenerator): """Search a grid defined by velocities and angles relative to the ecliptic. Attributes ---------- ecliptic_angle : `float` The angle to use for the ecliptic in the image (in the units defined in ``angle_units``). - vel_steps : `int` - The number of velocity steps. - min_vel : `float` - The minimum velocity magnitude (in pixels per day) - max_vel : `float` - The maximum velocity magnitude (in pixels per day) - ang_steps : `int` - The number of angle steps. - min_ang_offset : `float` - The minimum offset of the search angle relative from the ecliptic_angle (in radians). - max_ang_offset : `float` - The maximum offset of the search angle relative from the ecliptic_angle (in radians). + velocities : `list` + A triplet of the minimum velocity to use (in pixels per day), and the maximum velocity + magnitude (in pixels per day), and the number of velocity steps to try. + angles : `list` + A triplet of the minimum angle offset (in radians), and the maximum angle offset (in radians), + and the number of angles to try. min_ang : `float` The minimum search angle in the image (ecliptic_angle + min_ang_offset) in radians. max_ang : `float` @@ -361,39 +355,27 @@ class EclipticSearch(TrajectoryGenerator): def __init__( self, - vel_steps=0, - min_vel=0.0, - max_vel=0.0, - ang_steps=0, - min_ang_offset=0.0, - max_ang_offset=0.0, + velocities=[0.0, 0.0, 0], + angles=[0.0, 0.0, 0], angle_units="radians", - force_ecliptic=None, + given_ecliptic=None, computed_ecliptic_angle=None, **kwargs, ): - """Create a class KBMODV1Search. + """Create a class EclipticCenteredSearch. Parameters ---------- - vel_steps : `int` - The number of velocity steps. - min_vel : `float` - The minimum velocity magnitude (in pixels per day) - max_vel : `float` - The maximum velocity magnitude (in pixels per day) - ang_steps : `int` - The number of angle steps. - min_ang_offset : `float` - The minimum offset of the search angle relative from the ecliptic_angle - (in the units defined in ``angle_units``). - max_ang_offset : `float` - The maximum offset of the search angle relative from the ecliptic_angle - (in the units defined in ``angle_units``). + velocities : `list` + A triplet of the minimum velocity to use (in pixels per day), and the maximum velocity + magnitude (in pixels per day), and the number of velocity steps to try. + angles : `list` + A triplet of the minimum angle offset (in the units defined in ``angle_units``), the maximum + angle offset (in the units defined in ``angle_units``), and the number of angles to try. angle_units : `str` The units for the angle. Must be one of radians or degrees. Default: 'radians' - force_ecliptic : `float`, optional + given_ecliptic : `float`, optional An override for the ecliptic as given in the config (in the units defined in ``angle_units``). This angle takes precedence over ``computed_ecliptic``. computed_ecliptic_angle : `float`, optional @@ -402,8 +384,8 @@ def __init__( """ super().__init__(**kwargs) - if force_ecliptic is not None: - ecliptic_angle = force_ecliptic + if given_ecliptic is not None: + ecliptic_angle = given_ecliptic elif computed_ecliptic_angle is not None: ecliptic_angle = computed_ecliptic_angle else: @@ -411,48 +393,43 @@ def __init__( logger.warning("No ecliptic angle provided. Using 0.0.") ecliptic_angle = 0.0 - if vel_steps < 1 or ang_steps < 1: - raise ValueError("EclipticSearch requires at least 1 step in each dimension") - if max_vel < min_vel: - raise ValueError("Invalid EclipticSearch bounds.") + if velocities[2] < 1 or angles[2] < 1: + raise ValueError("EclipticCenteredSearch requires at least 1 step in each dimension") + if velocities[1] < velocities[0]: + raise ValueError(f"Invalid EclipticCenteredSearch bounds: {velocities}") - self.vel_steps = vel_steps - self.min_vel = min_vel - self.max_vel = max_vel - self.vel_stepsize = (self.max_vel - self.min_vel) / float(self.vel_steps - 1) + self.velocities = velocities + self.vel_stepsize = (velocities[1] - velocities[0]) / float(velocities[2] - 1) # Convert the angles into radians. - if angle_units[:3] == "rad": - self.ecliptic_angle = ecliptic_angle - self.min_ang_offset = min_ang_offset - self.max_ang_offset = max_ang_offset - elif angle_units[:3] == "deg": + self.angles = angles + self.ecliptic_angle = ecliptic_angle + if angle_units[:3] == "deg": deg_to_rad = math.pi / 180.0 - self.ecliptic_angle = deg_to_rad * ecliptic_angle - self.min_ang_offset = deg_to_rad * min_ang_offset - self.max_ang_offset = deg_to_rad * max_ang_offset - else: + self.ecliptic_angle = deg_to_rad * self.ecliptic_angle + self.angles[0] = deg_to_rad * self.angles[0] + self.angles[1] = deg_to_rad * self.angles[1] + elif angle_units[:3] != "rad": raise ValueError(f"Unknown angular units {angle_units}") - self.ang_steps = ang_steps - self.min_ang = self.ecliptic_angle + self.min_ang_offset - self.max_ang = self.ecliptic_angle + self.max_ang_offset - self.ang_stepsize = (self.max_ang - self.min_ang) / float(self.ang_steps - 1) + self.min_ang = self.ecliptic_angle + self.angles[0] + self.max_ang = self.ecliptic_angle + self.angles[1] + self.ang_stepsize = (self.max_ang - self.min_ang) / float(self.angles[2] - 1) def __repr__(self): return ( "EclipticSearch:" - f" v=[{self.min_vel}, {self.max_vel}], {self.vel_steps}" + f" v=[{self.velocities[0]}, {self.velocities[1]}], {self.velocities[2]}" f" a=[{self.min_ang}, {self.max_ang}], {self.ang_steps}" ) def __str__(self): return f"""EclipticSearch: - Vel: [{self.min_vel}, {self.max_vel}] in {self.vel_steps} steps. + Vel: [{self.velocities[0]}, {self.velocities[1]}] in {self.velocities[2]} steps. Ang: Ecliptic = {self.ecliptic_angle} - Offsets = {self.min_ang_offset} to {self.max_ang_offset} - [{self.min_ang}, {self.max_ang}] in {self.ang_steps} steps.""" + Offsets = {self.angles[0]} to {self.angles[1]} + [{self.min_ang}, {self.max_ang}] in {self.angles[2]} steps.""" def generate(self, *args, **kwargs): """Produces a single candidate trajectory to test. @@ -462,10 +439,10 @@ def generate(self, *args, **kwargs): candidate : `Trajectory` A ``Trajectory`` to test at each pixel. """ - for ang_i in range(self.ang_steps): - for vel_i in range(self.vel_steps): + for ang_i in range(self.angles[2]): + for vel_i in range(self.velocities[2]): curr_ang = self.min_ang + ang_i * self.ang_stepsize - curr_vel = self.min_vel + vel_i * self.vel_stepsize + curr_vel = self.velocities[0] + vel_i * self.vel_stepsize vx = math.cos(curr_ang) * curr_vel vy = math.sin(curr_ang) * curr_vel diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 3c7aeb18c..e93a49f91 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -52,11 +52,10 @@ def test_from_hdu(self): ["Here3"], ["7"], ["null"], - [ - "[1, 2]", - ], + ["[1, 2]"], + ["{name: test_gen, p1: [1.0, 2.0], p2: 2.0}"], ], - names=("im_filepath", "num_obs", "cluster_type", "ang_arr"), + names=("im_filepath", "num_obs", "cluster_type", "ang_arr", "generator_config"), ) hdu = fits.table_to_hdu(t) @@ -64,6 +63,9 @@ def test_from_hdu(self): self.assertEqual(config["im_filepath"], "Here3") self.assertEqual(config["num_obs"], 7) self.assertEqual(config["ang_arr"], [1, 2]) + self.assertEqual(config["generator_config"]["name"], "test_gen") + self.assertEqual(config["generator_config"]["p1"], [1.0, 2.0]) + self.assertEqual(config["generator_config"]["p2"], 2.0) self.assertIsNone(config["cluster_type"]) def test_to_hdu(self): @@ -73,7 +75,7 @@ def test_to_hdu(self): "cluster_type": None, "do_clustering": False, "res_filepath": "There", - "generator_config": {"name": "test_gen", "p1": 1.0, "p2": 2.0}, + "generator_config": {"name": "test_gen", "p1": [1.0, 2.0], "p2": 2.0}, "basic_array": [1.0, 2.0, 3.0], } config = SearchConfiguration.from_dict(d) @@ -83,7 +85,7 @@ def test_to_hdu(self): self.assertEqual(hdu.data["num_obs"][0], "5\n...") self.assertEqual(hdu.data["cluster_type"][0], "null\n...") self.assertEqual(hdu.data["res_filepath"][0], "There\n...") - self.assertEqual(hdu.data["generator_config"][0], "{name: test_gen, p1: 1.0, p2: 2.0}") + self.assertEqual(hdu.data["generator_config"][0], "{name: test_gen, p1: [1.0, 2.0], p2: 2.0}") self.assertEqual(hdu.data["basic_array"][0], "[1.0, 2.0, 3.0]") def test_to_yaml(self): @@ -93,7 +95,7 @@ def test_to_yaml(self): "cluster_type": None, "do_clustering": False, "res_filepath": "There", - "generator_config": {"name": "test_gen", "p1": 1.0, "p2": 2.0}, + "generator_config": {"name": "test_gen", "p1": [1.0, 2.0], "p2": 2.0}, } config = SearchConfiguration.from_dict(d) yaml_str = config.to_yaml() @@ -104,7 +106,7 @@ def test_to_yaml(self): self.assertEqual(yaml_dict["cluster_type"], None) self.assertEqual(yaml_dict["res_filepath"], "There") self.assertEqual(yaml_dict["generator_config"]["name"], "test_gen") - self.assertEqual(yaml_dict["generator_config"]["p1"], 1.0) + self.assertEqual(yaml_dict["generator_config"]["p1"], [1.0, 2.0]) self.assertEqual(yaml_dict["generator_config"]["p2"], 2.0) def test_save_and_load_yaml(self): diff --git a/tests/test_regression_test.py b/tests/test_regression_test.py index 883dd7a03..8fe864eac 100644 --- a/tests/test_regression_test.py +++ b/tests/test_regression_test.py @@ -245,16 +245,12 @@ def perform_search(im_stack, res_filename, default_psf): "psf_val": default_psf, "output_suffix": "", "generator_config": { - "name": "EclipticSearch", - "vel_steps": 52, - "min_vel": 92.0, - "max_vel": 550.0, - "ang_steps": 26, + "name": "EclipticCenteredSearch", # Offset by PI for prograde orbits in lori allen data - "min_ang_offset": np.pi - np.pi / 10.0, - "max_ang_offset": np.pi + np.pi / 10.0, + "angles": [np.pi - np.pi / 10.0, np.pi + np.pi / 10.0, 26], + "velocities": [92.0, 550.0, 52], "angle_units": "radians", - "force_ecliptic": 1.1901106654050821, + "given_ecliptic": 1.1901106654050821, }, "num_obs": 15, "do_mask": True, diff --git a/tests/test_trajectory_generator.py b/tests/test_trajectory_generator.py index e66706732..34b16d6c7 100644 --- a/tests/test_trajectory_generator.py +++ b/tests/test_trajectory_generator.py @@ -5,7 +5,7 @@ from kbmod.trajectory_generator import ( KBMODV1Search, KBMODV1SearchConfig, - EclipticSearch, + EclipticCenteredSearch, SingleVelocitySearch, RandomVelocitySearch, VelocityGridSearch, @@ -70,8 +70,10 @@ def test_KBMODV1Search(self): self.assertRaises(ValueError, KBMODV1Search, 3, 0.0, 3.0, 2, 0.25, -0.25) self.assertRaises(ValueError, KBMODV1Search, 3, 3.5, 3.0, 2, -0.25, 0.25) - def test_EclipticSearch(self): - gen = EclipticSearch(3, 0.0, 2.0, 3, -45.0, 45.0, angle_units="degrees", ecliptic_angle=0.0) + def test_EclipticCenteredSearch(self): + gen = EclipticCenteredSearch( + [0.0, 2.0, 3], [-45.0, 45.0, 3], angle_units="degrees", given_ecliptic=0.0 + ) expected_x = [0.0, 0.707107, 1.41421, 0.0, 1.0, 2.0, 0.0, 0.707107, 1.41421] expected_y = [0.0, -0.707107, -1.41421, 0.0, 0.0, 0.0, 0.0, 0.707107, 1.41421] @@ -89,9 +91,9 @@ def test_EclipticSearch(self): self.assertAlmostEqual(tbl["vy"][i], expected_y[i], delta=0.001) # Test invalid number of steps and ranges. - self.assertRaises(ValueError, EclipticSearch, 0.0, 3, 0.0, 3.0, 0, -0.25, 0.25) - self.assertRaises(ValueError, EclipticSearch, 0.0, 0, 0.0, 3.0, 2, -0.25, 0.25) - self.assertRaises(ValueError, EclipticSearch, 0.0, 3, 3.5, 3.0, 2, -0.25, 0.25) + self.assertRaises(ValueError, EclipticCenteredSearch, [0.0, 3.0, 3], [-0.25, 0.25, 0]) + self.assertRaises(ValueError, EclipticCenteredSearch, [0.0, 3.0, 0], [-0.25, 0.25, 2]) + self.assertRaises(ValueError, EclipticCenteredSearch, [3.5, 3.0, 3], [-0.25, 0.25, 2]) def test_KBMODV1SearchConfig(self): # Note that KBMOD v1's search will never include the upper bound of angle or velocity. @@ -154,23 +156,19 @@ def test_create_trajectory_generator(self): # Test we can create a trajectory generator with optional keyword parameters. config3 = { - "name": "EclipticSearch", - "vel_steps": 2, - "min_vel": 0.0, - "max_vel": 1.0, - "ang_steps": 2, - "min_ang_offset": 0.0, - "max_ang_offset": 45.0, + "name": "EclipticCenteredSearch", + "angles": [0.0, 45.0, 2], + "velocities": [0.0, 1.0, 2], "angle_units": "degrees", "force_ecliptic": None, } gen3 = create_trajectory_generator(config3, computed_ecliptic_angle=45.0) - self.assertTrue(type(gen3) is EclipticSearch) + self.assertTrue(type(gen3) is EclipticCenteredSearch) self.assertAlmostEqual(gen3.ecliptic_angle, np.pi / 4.0) self.assertEqual(gen3.min_ang, np.pi / 4.0) self.assertEqual(gen3.max_ang, np.pi / 2.0) - config3["force_ecliptic"] = 0.0 + config3["given_ecliptic"] = 0.0 gen4 = create_trajectory_generator(config3, computed_ecliptic_angle=45.0) self.assertAlmostEqual(gen4.ecliptic_angle, 0.0) self.assertEqual(gen4.min_ang, 0.0)