Skip to content

Commit 189e886

Browse files
Merge pull request #97 from lsst/tickets/SP-2540
tickets/SP-2540: add photometry evaluation
2 parents cb59905 + 6c6e5a7 commit 189e886

1 file changed

Lines changed: 174 additions & 39 deletions

File tree

MPC/RFL_Solar_System_MPC_Retrieval.ipynb

Lines changed: 174 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@
1818
"\n",
1919
"</div>\n",
2020
"\n",
21-
"For the Rubin Science Platform at data.lsst.cloud. <br>\n",
22-
"For Solar System object discoveries released as part of Rubin First Look. <br>\n",
23-
"Container Size: large <br>\n",
24-
"LSST Science Pipelines version: r29.2.0 <br>\n",
25-
"Last verified to run: 2025-09-02 <br>\n",
26-
"Repository: <a href=\"https://github.com/lsst/tutorial-notebooks\">github.com/lsst/tutorial-notebooks</a> <br>\n"
21+
"For the Rubin Science Platform at data.lsst.cloud.\\\n",
22+
"For Solar System object discoveries released as part of Rubin First Look.\\\n",
23+
"Container Size: large\\\n",
24+
"LSST Science Pipelines version: r29.2.0\\\n",
25+
"Last verified to run: 2025-11-25\\\n",
26+
"Repository: [github.com/lsst/tutorial-notebooks](https://github.com/lsst/tutorial-notebooks)\\\n",
27+
"DOI: [10.11578/rubin/dc.20250909.20](https://doi.org/10.11578/rubin/dc.20250909.20)"
2728
]
2829
},
2930
{
@@ -35,14 +36,14 @@
3536
"\n",
3637
"**LSST data products:** Small body observations made by Rubin, queried from the Minor Planet Center (MPC), and orbits computed by the MPC from those Rubin observations are used in the notebook. No direct LSST data products are used in this notebook.\n",
3738
"\n",
38-
"**Packages:** `get_orb` and `get_obs` <a href=\"https://www.minorplanetcenter.net/mpcops/documentation/\">APIs from the Minor Planet Center</a>.\n",
39+
"**Packages:** `get_orb` and `get_obs` [APIs from the Minor Planet Center](https://www.minorplanetcenter.net/mpcops/documentation/).\n",
3940
"\n",
4041
"**Credit:**\n",
4142
"Originally developed by Sarah Greenstreet and Mario Juric from the Rubin Community Science team, Solar System Processing (SSP) team and Solar System Commissioning science unit. Please consider acknowledging them if this notebook is used for the preparation of journal articles, software releases, or other notebooks.\n",
4243
"\n",
4344
"**Get Support:**\n",
44-
"Everyone is encouraged to ask questions or raise issues in the <a href=\"https://community.lsst.org/c/support\">Support Category</a> \n",
45-
"of the Rubin Community Forum. Rubin staff will respond to all questions posted there."
45+
"Everyone is encouraged to ask questions or raise issues in the [Support Category](https://community.lsst.org/c/support) of the Rubin Community Forum.\n",
46+
"Rubin staff will respond to all questions posted there."
4647
]
4748
},
4849
{
@@ -66,11 +67,8 @@
6667
"source": [
6768
"### 1.1. Import packages\n",
6869
"\n",
69-
"Import `numpy`, a fundamental package for scientific computing with arrays in Python\n",
70-
"(<a href=\"https://numpy.org\">numpy.org</a>), and\n",
71-
"`matplotlib`, a comprehensive library for data visualization\n",
72-
"(<a href=\"https://matplotlib.org/\">matplotlib.org</a>; \n",
73-
"<a href=\"https://matplotlib.org/stable/gallery/index.html\">matplotlib gallery</a>)."
70+
"Import `numpy`, a fundamental package for scientific computing with arrays in Python ([numpy.org](https://numpy.org)), and\n",
71+
"`matplotlib`, a comprehensive library for data visualization ([matplotlib.org](https://matplotlib.org/); [matplotlib gallery](https://matplotlib.org/stable/gallery/index.html))."
7472
]
7573
},
7674
{
@@ -84,7 +82,9 @@
8482
"import numpy as np\n",
8583
"import pandas as pd\n",
8684
"import matplotlib.pyplot as plt\n",
87-
"from astropy.time import Time"
85+
"from astropy.time import Time\n",
86+
"from lsst.utils.plotting import (get_multiband_plot_colors,\n",
87+
" get_multiband_plot_symbols)"
8888
]
8989
},
9090
{
@@ -143,6 +143,26 @@
143143
"colors = prop_cycle.by_key()['color']"
144144
]
145145
},
146+
{
147+
"cell_type": "markdown",
148+
"id": "fd91df61-fb09-48e4-88a1-bd9fa6786aec",
149+
"metadata": {},
150+
"source": [
151+
"Define the colors and symbols to use in plots to represent the six LSST filters, $ugrizy$."
152+
]
153+
},
154+
{
155+
"cell_type": "code",
156+
"execution_count": null,
157+
"id": "f9d6274f-1d2f-428e-b84b-2d3a8fb79fb9",
158+
"metadata": {},
159+
"outputs": [],
160+
"source": [
161+
"filter_names = ['u', 'g', 'r', 'i', 'z', 'y']\n",
162+
"filter_colors = get_multiband_plot_colors()\n",
163+
"filter_symbols = get_multiband_plot_symbols()"
164+
]
165+
},
146166
{
147167
"cell_type": "markdown",
148168
"id": "6d5dae6e-e177-497a-b61f-b078809ae987",
@@ -160,7 +180,7 @@
160180
"id": "dd380a4c-858b-42cf-bd2c-43fb9311c96a",
161181
"metadata": {},
162182
"source": [
163-
"### 2.1 Query the Minor Planet Center\n",
183+
"### 2.1. Query the Minor Planet Center\n",
164184
"\n",
165185
"Use the Minor Planet Center's `get_orb` API to retrieve the orbit information for all 2,098 objects in the Rubin First Look Solar System discoveries and store the list of retrieved dictionaries in a DataFrame called `discovery_orbits`. Return fields for the object's provisional designation, absolute magnitude, semimajor axis, eccentricity, inclination, argument of perihelion, longitude of ascending node, time of perihelion passage, the total number of observations used in the orbit fit, and the orbit classification information. The request may take roughly 10 minutes or more to complete.\n",
166186
"\n",
@@ -238,15 +258,16 @@
238258
"id": "b57066e8-f27d-4f1b-932f-529f4bb75cb7",
239259
"metadata": {},
240260
"source": [
241-
"### 2.2 Plot orbit classifications"
261+
"### 2.2. Plot orbit classifications"
242262
]
243263
},
244264
{
245265
"cell_type": "markdown",
246266
"id": "a928a607-9977-4a09-b689-9efeb49b8437",
247267
"metadata": {},
248268
"source": [
249-
"Plot the semimajor axes, eccentricities, and inclinations of the 2,098 Rubin First Look Solar System object discoveries. Use the orbit types defined by the MPC (https://minorplanetcenter.net/mpcops/documentation/orbit-types/) to distinguish each orbital classification in the plot.\n"
269+
"Plot the semimajor axes, eccentricities, and inclinations of the 2,098 Rubin First Look Solar System object discoveries.\n",
270+
"Use the [orbit types defined by the MPC](https://minorplanetcenter.net/mpcops/documentation/orbit-types/) to distinguish each orbital classification in the plot."
250271
]
251272
},
252273
{
@@ -256,7 +277,9 @@
256277
"metadata": {},
257278
"outputs": [],
258279
"source": [
259-
"a, e, inc = discovery_orbits[\"semimajor_axis\"], discovery_orbits[\"eccentricity\"], discovery_orbits[\"inclination\"]\n",
280+
"a = discovery_orbits[\"semimajor_axis\"]\n",
281+
"e = discovery_orbits[\"eccentricity\"]\n",
282+
"inc = discovery_orbits[\"inclination\"]\n",
260283
"inner_orbits = discovery_orbits[a < 7]\n",
261284
"outer_orbits = discovery_orbits[a >= 7]\n",
262285
"\n",
@@ -279,7 +302,7 @@
279302
" 99: \"Other\"\n",
280303
"}\n",
281304
"\n",
282-
"fig, axes = plt.subplots(1, 2, figsize=(8,4))\n",
305+
"fig, axes = plt.subplots(1, 2, figsize=(8, 4))\n",
283306
"plt.subplots_adjust(wspace=0.25)\n",
284307
"\n",
285308
"for ax, oo in zip(axes, (inner_orbits, outer_orbits)):\n",
@@ -291,11 +314,12 @@
291314
" s = 10\n",
292315
" marker, markers = markers[0], markers[1:]\n",
293316
" label = orbit_type_map[otype]\n",
294-
" ax.scatter(orb[\"semimajor_axis\"], orb[\"eccentricity\"], s=s, marker=marker, label=f\"{label} ({len(orb)})\")\n",
317+
" ax.scatter(orb[\"semimajor_axis\"], orb[\"eccentricity\"], s=s,\n",
318+
" marker=marker, label=f\"{label} ({len(orb)})\")\n",
295319
"\n",
296320
" for handle in ax.legend().legend_handles:\n",
297321
" handle.set_sizes([10])\n",
298-
" ax.legend(loc = 'upper right', bbox_to_anchor=(0.92, 1), fontsize=6)\n",
322+
" ax.legend(loc='upper right', bbox_to_anchor=(0.92, 1), fontsize=6)\n",
299323
" ax.set_xlabel(\"semimajor axis (au)\")\n",
300324
" ax.set_ylabel(\"eccentricity\")\n",
301325
"\n",
@@ -313,11 +337,12 @@
313337
" s = 10\n",
314338
" marker, markers = markers[0], markers[1:]\n",
315339
" label = orbit_type_map[otype]\n",
316-
" ax.scatter(orb[\"semimajor_axis\"], orb[\"inclination\"], s=s, marker=marker, label=f\"{label} ({len(orb)})\")\n",
340+
" ax.scatter(orb[\"semimajor_axis\"], orb[\"inclination\"], s=s,\n",
341+
" marker=marker, label=f\"{label} ({len(orb)})\")\n",
317342
"\n",
318343
" for handle in ax.legend().legend_handles:\n",
319344
" handle.set_sizes([10])\n",
320-
" ax.legend(loc = 'upper right', fontsize=6)\n",
345+
" ax.legend(loc='upper right', fontsize=6)\n",
321346
" ax.set_xlabel(\"semimajor axis (au)\")\n",
322347
" ax.set_ylabel(\"inclination (deg)\")"
323348
]
@@ -351,7 +376,7 @@
351376
"id": "2edbdaba-551d-4b72-a346-16227a9c45fb",
352377
"metadata": {},
353378
"source": [
354-
"### 3.1 Query the Minor Planet Center\n",
379+
"### 3.1. Query the Minor Planet Center\n",
355380
"\n",
356381
"Use the Minor Planet Center's `get_obs` API to retrieve all observations for the 2,098 objects in the Rubin First Look Solar System discoveries and store them in a pandas DataFrame called `discovery_observations`, reducing the number of columns to those needed and converting selected strings to floats using numpy's `to_numeric` module. The request may take up to 40 minutes or more to complete."
357382
]
@@ -383,7 +408,7 @@
383408
"\n",
384409
"for i, obj in enumerate(targets[1:]):\n",
385410
" response = requests.get(\"https://data.minorplanetcenter.net/api/get-obs\",\n",
386-
" json={\"desigs\": [obj], \"output_format\":[\"ADES_DF\"]})\n",
411+
" json={\"desigs\": [obj], \"output_format\": [\"ADES_DF\"]})\n",
387412
" if response.ok:\n",
388413
" ades_df = pd.DataFrame(response.json()[0]['ADES_DF'])\n",
389414
" else:\n",
@@ -399,11 +424,12 @@
399424
" 'poscov13', 'poscov22', 'poscov23',\n",
400425
" 'poscov33', 'precdec', 'precra',\n",
401426
" 'prectime', 'prog', 'rastar', 'rcv',\n",
402-
" 'ref', 'remarks', 'rmscorr','rmsdelay',\n",
427+
" 'ref', 'remarks', 'rmscorr', 'rmsdelay',\n",
403428
" 'rmsdist', 'rmsdoppler', 'rmsfit',\n",
404429
" 'rmspa', 'rmstime', 'seeing', 'subfmt',\n",
405430
" 'subfrm', 'sys', 'trkid', 'trx', 'unctime'], axis=1)\n",
406-
" discovery_observations = pd.concat([discovery_observations, ingest_observations], ignore_index=True)\n",
431+
" discovery_observations = pd.concat([discovery_observations, ingest_observations],\n",
432+
" ignore_index=True)\n",
407433
" discovery_observations['dec'] = pd.to_numeric(discovery_observations['dec'])\n",
408434
" discovery_observations['ra'] = pd.to_numeric(discovery_observations['ra'])\n",
409435
" discovery_observations['rmsdec'] = pd.to_numeric(discovery_observations['rmsdec'])\n",
@@ -482,7 +508,7 @@
482508
"id": "2a885ec6-9861-435b-9025-998005fa6735",
483509
"metadata": {},
484510
"source": [
485-
"### 3.2 Examine the magnitude variation for an example discovery\n",
511+
"### 3.2. Examine the magnitude variation for an example discovery\n",
486512
"\n",
487513
"Sort `discovery_orbits` by number of observations in descending order to find the objects with the largest number of observations. Print the 10 objects with the highest number of observations."
488514
]
@@ -494,7 +520,8 @@
494520
"metadata": {},
495521
"outputs": [],
496522
"source": [
497-
"discovery_orbits_observations_sorted_desc = discovery_orbits.sort_values(by='nobs_total', ascending=False)\n",
523+
"discovery_orbits_observations_sorted_desc = discovery_orbits.sort_values(by='nobs_total',\n",
524+
" ascending=False)\n",
498525
"discovery_orbits_observations_sorted_desc.head(10)"
499526
]
500527
},
@@ -513,7 +540,8 @@
513540
"metadata": {},
514541
"outputs": [],
515542
"source": [
516-
"select = np.isin(Rubin_discovery_observations['provid'], '2025 MX21', invert=False)\n",
543+
"object_name = '2025 MX21'\n",
544+
"select = np.isin(Rubin_discovery_observations['provid'], object_name, invert=False)\n",
517545
"selected_Rubin_discovery_observations = Rubin_discovery_observations[select].copy()\n",
518546
"len(selected_Rubin_discovery_observations)"
519547
]
@@ -534,26 +562,37 @@
534562
"outputs": [],
535563
"source": [
536564
"MJDmin, MJDmax = 60797, 60797.2\n",
537-
"MJD, mag = selected_Rubin_discovery_observations['epoch'], selected_Rubin_discovery_observations['mag']\n",
565+
"MJD = selected_Rubin_discovery_observations['epoch']\n",
566+
"mag = selected_Rubin_discovery_observations['mag']\n",
538567
"full_obs = selected_Rubin_discovery_observations\n",
539568
"single_night_obs = selected_Rubin_discovery_observations[(MJDmin < MJD) & (MJD < MJDmax)]\n",
540569
"\n",
541570
"fig, axes = plt.subplots(1, 2, figsize=(14, 4))\n",
542571
"\n",
572+
"axes[0].axvline(MJDmin + 0.1, lw=10, alpha=0.2, color='grey')\n",
573+
"a = 0\n",
543574
"for ax, mags in zip(axes, (full_obs, single_night_obs)):\n",
544-
" markers = \"s^*+xDh\"*2\n",
545-
" for label, mag in mags.groupby('band'):\n",
546-
" s = 10\n",
547-
" marker, markers = markers[0], markers[1:]\n",
548-
" ax.scatter(mag['epoch'], mag['mag'], s=s, marker=marker, label=f\"{label} ({len(mag)})\")\n",
575+
" for filt in ['g', 'r', 'i']:\n",
576+
" select_filt = np.isin(mags['band'], filt, invert=False)\n",
577+
" tx = np.where(select_filt == 1)[0]\n",
578+
" ax.scatter(mags['epoch'][select_filt], mags['mag'][select_filt],\n",
579+
" s=10, marker=filter_symbols[filt],\n",
580+
" color=filter_colors[filt],\n",
581+
" label=f\"{filt} ({len(tx)})\")\n",
582+
" if a == 1:\n",
583+
" ax.errorbar(mags['epoch'][select_filt], mags['mag'][select_filt],\n",
584+
" yerr=mags['rmsmag'][select_filt], fmt='None',\n",
585+
" ecolor=filter_colors[filt])\n",
586+
" del select_filt, tx\n",
587+
" a += 1\n",
549588
"\n",
550589
" for handle in ax.legend().legend_handles:\n",
551590
" handle.set_sizes([10])\n",
552591
" ax.invert_yaxis()\n",
553592
" ax.set_xlabel('MJD')\n",
554593
" ax.set_ylabel('Magnitude')\n",
555594
" ax.minorticks_on()\n",
556-
"fig.suptitle(f\"Rubin First Look Solar System Object Discovery {selected_Rubin_discovery_observations['provid'].values[0]} Magnitude Variation\")\n",
595+
"fig.suptitle(f\"Rubin First Look Solar System Object Discovery {object_name} Magnitude Variation\")\n",
557596
"\n",
558597
"plt.show()"
559598
]
@@ -566,10 +605,106 @@
566605
"> **Figure 2:** Magnitude over time for the selected Rubin First Look Solar System object discovery both for the full observation period (left) and on a single night (right) to see the magnitude variation. Observations were taken in $g$-, $r$-, and $i$-band."
567606
]
568607
},
608+
{
609+
"cell_type": "markdown",
610+
"id": "8af622a5-3d13-4a27-9c99-393360927989",
611+
"metadata": {},
612+
"source": [
613+
"### 3.3. Evaluate photometric quality\n",
614+
"\n",
615+
"In a full Rubin data release, the photometry for moving objects in the `DiaSource` catalog comes with flag parameters to indicate potential issues with the photometric measurement or calibration (e.g., contamination from cosmic ray flux, sources at the edge of a detector), and the difference- and direct-images are available for visual checks on the source being measured.\n",
616+
"Observational metadata such as seeing, sky background, and limiting metadata are also available.\n",
617+
"These aspects of a full Rubin data release enable users to identify and reject potential poor-quality measurements from an analysis.\n",
618+
"\n",
619+
"However, for the RFL photometry released to the MPC, the only indicator of photometric quality available is the `rmsmag` column."
620+
]
621+
},
622+
{
623+
"cell_type": "markdown",
624+
"id": "590e0d1f-d15b-4944-8a36-76d1091fdd73",
625+
"metadata": {},
626+
"source": [
627+
"Show the distribution of all `rmsmag` values in order to approximate the boundary between the distribution of \"acceptable\" or \"normal\" measurements, and the tail of \"outlier\" measurements."
628+
]
629+
},
630+
{
631+
"cell_type": "code",
632+
"execution_count": null,
633+
"id": "738421d1-3180-4a67-9bf5-4537e083294b",
634+
"metadata": {},
635+
"outputs": [],
636+
"source": [
637+
"fig = plt.figure(figsize=(8, 3))\n",
638+
"plt.axvline(0.25, ls='dashed', alpha=0.7, color='black')\n",
639+
"for filt in ['g', 'r', 'i']:\n",
640+
" select_filt = np.isin(Rubin_discovery_observations['band'],\n",
641+
" filt, invert=False)\n",
642+
" plt.hist(Rubin_discovery_observations['rmsmag'][select_filt], 80,\n",
643+
" histtype='step', log=True, color=filter_colors[filt], label=filt)\n",
644+
"plt.xlabel('rms mag')\n",
645+
"plt.ylabel('log(N)')\n",
646+
"plt.legend(loc='upper right')\n",
647+
"plt.show()"
648+
]
649+
},
650+
{
651+
"cell_type": "markdown",
652+
"id": "4a447a05-0d3a-435a-a28a-2b1f45865444",
653+
"metadata": {},
654+
"source": [
655+
"> **Figure 3:** The log of the number of photometric measurements with a given `rmsmag` value. The dashed grey line is drawn at $0.25$ mag, the approximate (by-eye) division between the distribution of `rmsmag` values and the tail of \"outliers\"."
656+
]
657+
},
658+
{
659+
"cell_type": "markdown",
660+
"id": "dd3811a9-a6cb-4649-aba1-e4cc060de302",
661+
"metadata": {},
662+
"source": [
663+
"Show scatter plots of the `rmsmag` values as a function of `mag` values, per filter."
664+
]
665+
},
666+
{
667+
"cell_type": "code",
668+
"execution_count": null,
669+
"id": "94881aa2-32d8-4647-92c7-e6bbde01209b",
670+
"metadata": {},
671+
"outputs": [],
672+
"source": [
673+
"fig, axes = plt.subplots(1, 3, figsize=(10, 5))\n",
674+
"for f, filt in enumerate(['g', 'r', 'i']):\n",
675+
" select = np.isin(Rubin_discovery_observations['band'], filt, invert=False)\n",
676+
" axes[f].axhline(0.25, alpha=0.5, color='grey')\n",
677+
" axes[f].plot(Rubin_discovery_observations['mag'][select],\n",
678+
" Rubin_discovery_observations['rmsmag'][select],\n",
679+
" 'o', ms=2, mew=0, alpha=0.7, color=filter_colors[filt])\n",
680+
" axes[f].set_xlabel('mag')\n",
681+
" axes[f].set_title(filt)\n",
682+
"axes[0].set_ylabel('rms mag')\n",
683+
"plt.show()"
684+
]
685+
},
686+
{
687+
"cell_type": "markdown",
688+
"id": "1d196cd2-5a3a-4e66-8308-a0cd1f23106c",
689+
"metadata": {},
690+
"source": [
691+
"> **Figure 4:** The `rmsmag` as a function of `mag` for the $g$-, $r$-, and $i$-band photometric measurements. The main locus of points in each plot represents \"normal\" photometric measurements, and the outliers are clearly visible. The horizontal lines drawn at `rmsmag` $=0.25$ mag represent about the upper bound on the \"inliers\"."
692+
]
693+
},
694+
{
695+
"cell_type": "markdown",
696+
"id": "07571984-17db-4c2d-b297-96d79f088909",
697+
"metadata": {},
698+
"source": [
699+
"The above demonstrates a simple, by-eye evaluation that `rmsmag` $> 0.25$ mag indicates an \"outlier\" photometric meaurement for the RFL data sent to the MPC.\n",
700+
"\n",
701+
"Inclusion of these data points in any analysis should be done with caution."
702+
]
703+
},
569704
{
570705
"cell_type": "code",
571706
"execution_count": null,
572-
"id": "9eb55b51-a84b-4404-bea1-ff593692aa69",
707+
"id": "ab073769-fba2-4821-82c9-141dca24a75d",
573708
"metadata": {},
574709
"outputs": [],
575710
"source": []

0 commit comments

Comments
 (0)