{
  "id": 609443,
  "title": "9th Solution",
  "url": "/competitions/ariel-data-challenge-2025/discussion/609443",
  "author_name": "CPMP",
  "post_date": "2025-09-26T17:06:18.076000",
  "votes": 23,
  "comment_count": 6,
  "views": 0,
  "content": "<p>Thanks to kaggle and everyone involved for hosting such an interesting competition. There was a lot to learn, and that's why we compete. </p>\n<p><strong>TLDR</strong></p>\n<p>Our solution is a a mix of transit light curve curve fitting using either batman + scipy-opitimize or pylightcurve-torch + gpytorch. We then trained a linear NN to calibrate predictions and estimate sigma values.</p>\n<p>We only discovered Pylightcurve-torch and GPytorch a week before the end. We then sprinted to replicate and improve the batman pipeline with these. </p>\n<p><strong>Cross validation</strong></p>\n<p>Cross validation was straightforward. We simply split the star into 4 folds. We evaluated the curve fitting steps without CV as there was no training, but for the NN postprocessing we did proper CV evaluation. Overall we had very good CV / LB correlation.</p>\n<p><strong>Data preprocessing</strong></p>\n<p>We used <a href=\"https://www.kaggle.com/ilu000\" target=\"_blank\">@ilu000</a> preprocessing as a basis which has been shared here: <a href=\"https://www.kaggle.com/code/ilu000/ariel25-quick-data-prep-improved\" target=\"_blank\">https://www.kaggle.com/code/ilu000/ariel25-quick-data-prep-improved</a></p>\n<p>We added a minor tweak for detecting cosmic rays, which was also shared in <a href=\"https://www.kaggle.com/jeroencottaar\" target=\"_blank\">@jeroencottaar</a> notebook <a href=\"https://www.kaggle.com/code/jeroencottaar/demonstrate-apparent-label-issue\" target=\"_blank\">https://www.kaggle.com/code/jeroencottaar/demonstrate-apparent-label-issue</a></p>\n<p>We made the additional following changes. They yield over 0.005 score improvement when we implement them. </p>\n<ul>\n<li>Inverted offset. The formula provided to use at the start of the competition was the formula used to encode the data. Given we had to decode, we have to invert both operations in the formula. Host only fixed one inversion during the competition. </li>\n<li>The dark frame must be multiplied by the duration since sensor reset. We therefore updated the code this way:</li>\n</ul>\n<pre><code>        \n         sensor == :\n            dt = torch.ones((signal), device=) * \n            dt[::] += \n         sensor == :\n            dt = torch.tensor(dt_airs).to()\n            dt[::] += \n</code></pre>\n<ul>\n<li>Jitter removal. We centered the signal by computing the center oliver spatial dimensions, then using scipy 2d interpolate to move the center to the center of the frame. Doing so we need to crop the outermost pixels.</li>\n</ul>\n<p>Here is an example of correcting AIRS center on the spatial axis. Orange is after correction.</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F75976%2F0db2d0135a74f942ed6687fd04eabceb%2FScreenshot%202025-09-26%20at%2019-05-26%20exomod_534_a%20(auto-c%2017)%20-%20JupyterLab.png?generation=1758906368284879&amp;alt=media\" alt=\"\"></p>\n<ul>\n<li>Some frames have very large center deviations. This is because of cosmic rays. We simply discard these frames and replace them by the one right before it. Here is an example of two cosmic rays effect on FGS1.</li>\n</ul>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F75976%2Faff24e5206a0ef5d63a49ca2d97cfb14%2FScreenshot%202025-09-26%20at%2019-08-18%20exomod_531_f%20(auto-c%2013)%20-%20JupyterLab.png?generation=1758906523858560&amp;alt=media\" alt=\"\"></p>\n<ul>\n<li>We did not fix dispersion changes due to Jitter. It can still be significant. Here is the same planet where we plot the normalized variance of signal in one of FGS1 spatial axis. The bump in the middle means that more signal will be cropped from a narrow square.</li>\n</ul>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F75976%2F90992f5522784a9cdd749e78954af53c%2FScreenshot%202025-09-26%20at%2019-09-25%20exomod_531_f%20(auto-c%2013)%20-%20JupyterLab.png?generation=1758906657017925&amp;alt=media\" alt=\"\"></p>\n<p>-Light is not appearing as a nice ellipsis on the FGS1 sensor. Here is the log scaled average over all planets of FGS1 signal, after centering it:</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F75976%2F6c4cf9b3028bead1d0d2373b1b4e2007%2FScreenshot%202025-09-26%20at%2019-11-39%20exomod_prepr%20(auto-c%2012)%20-%20JupyterLab.png?generation=1758906716843836&amp;alt=media\" alt=\"\"></p>\n<p>We masked the darker part of that average, rather than using a square crop of the center.</p>\n<p><strong>Main approach</strong></p>\n<p>We employed an iterative curve-fitting strategy to extract the planet-to-star radius ratio (Rp/Rs) from noisy transient light curve data. The procedure consisted of progressively refining the fit across different data representations:</p>\n<ol>\n<li>Phase detection – The orbital phase of the signal was identified to align the transit features. We used the largest gradient average over wavelengths, as in public notebooks.</li>\n<li>Baseline modeling – A degree 2 polynomial fit was applied to the out-of-transit “arms” of the light curve to model and remove baseline trends.</li>\n<li>Global transit fitting – The normalized light curve was fit using the batman transit model, yielding an initial estimate of Rp.</li>\n<li>FGS refinement – The fit was repeated on the FGS data</li>\n<li>Wavelength binning – The same fitting procedure was applied to 18 wavelength buckets, capturing wavelength-dependent variations.</li>\n<li>Wavelength binning with GP – For the bucketed data, a 2D polynomial plus Gaussian Process (GP) model was used to jointly capture systematic trends and correlated noise, refining the Rp estimate.</li>\n</ol>\n<p><strong>Postprocessing</strong></p>\n<ol>\n<li>combine predictions. We combined the results from the different curve fitting steps into 18 values for prediction and 283 values for sigma; those then go into a linear NN which weights the different sigma estimates and expands the 18 predictions back to 283 values</li>\n<li>train simple NN for calibrating predictions and sigma.. We use a NN because we used a GLLoss instead of the usual MSE for linear regression.</li>\n</ol>\n<pre><code> torch\n torch.nn  nn\n torch.nn.functional  F\n\n\n (nn.Module):\n     ():\n        ().__init__()\n        .pred_fc = nn.Linear(, , bias=)\n        .sigma_fc = nn.Linear(, , bias=)\n\n     ():\n        x = torch.log(pred).()\n        x = .pred_fc(x).squeeze(-)\n\n        y = torch.stack([sigma, s_std, var_l, s_std2, s_std3], ).()\n        y = torch.log(y)\n        y = .sigma_fc(y).squeeze(-)\n         x, y\n</code></pre>",
  "messages": [
    {
      "id": 3294767,
      "postDate": "2025-09-26T17:06:18.077Z",
      "content": "<p>Thanks to kaggle and everyone involved for hosting such an interesting competition. There was a lot to learn, and that's why we compete. </p>\n<p><strong>TLDR</strong></p>\n<p>Our solution is a a mix of transit light curve curve fitting using either batman + scipy-opitimize or pylightcurve-torch + gpytorch. We then trained a linear NN to calibrate predictions and estimate sigma values.</p>\n<p>We only discovered Pylightcurve-torch and GPytorch a week before the end. We then sprinted to replicate and improve the batman pipeline with these. </p>\n<p><strong>Cross validation</strong></p>\n<p>Cross validation was straightforward. We simply split the star into 4 folds. We evaluated the curve fitting steps without CV as there was no training, but for the NN postprocessing we did proper CV evaluation. Overall we had very good CV / LB correlation.</p>\n<p><strong>Data preprocessing</strong></p>\n<p>We used <a href=\"https://www.kaggle.com/ilu000\" target=\"_blank\">@ilu000</a> preprocessing as a basis which has been shared here: <a href=\"https://www.kaggle.com/code/ilu000/ariel25-quick-data-prep-improved\" target=\"_blank\">https://www.kaggle.com/code/ilu000/ariel25-quick-data-prep-improved</a></p>\n<p>We added a minor tweak for detecting cosmic rays, which was also shared in <a href=\"https://www.kaggle.com/jeroencottaar\" target=\"_blank\">@jeroencottaar</a> notebook <a href=\"https://www.kaggle.com/code/jeroencottaar/demonstrate-apparent-label-issue\" target=\"_blank\">https://www.kaggle.com/code/jeroencottaar/demonstrate-apparent-label-issue</a></p>\n<p>We made the additional following changes. They yield over 0.005 score improvement when we implement them. </p>\n<ul>\n<li>Inverted offset. The formula provided to use at the start of the competition was the formula used to encode the data. Given we had to decode, we have to invert both operations in the formula. Host only fixed one inversion during the competition. </li>\n<li>The dark frame must be multiplied by the duration since sensor reset. We therefore updated the code this way:</li>\n</ul>\n<pre><code>        \n         sensor == :\n            dt = torch.ones((signal), device=) * \n            dt[::] += \n         sensor == :\n            dt = torch.tensor(dt_airs).to()\n            dt[::] += \n</code></pre>\n<ul>\n<li>Jitter removal. We centered the signal by computing the center oliver spatial dimensions, then using scipy 2d interpolate to move the center to the center of the frame. Doing so we need to crop the outermost pixels.</li>\n</ul>\n<p>Here is an example of correcting AIRS center on the spatial axis. Orange is after correction.</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F75976%2F0db2d0135a74f942ed6687fd04eabceb%2FScreenshot%202025-09-26%20at%2019-05-26%20exomod_534_a%20(auto-c%2017)%20-%20JupyterLab.png?generation=1758906368284879&amp;alt=media\" alt=\"\"></p>\n<ul>\n<li>Some frames have very large center deviations. This is because of cosmic rays. We simply discard these frames and replace them by the one right before it. Here is an example of two cosmic rays effect on FGS1.</li>\n</ul>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F75976%2Faff24e5206a0ef5d63a49ca2d97cfb14%2FScreenshot%202025-09-26%20at%2019-08-18%20exomod_531_f%20(auto-c%2013)%20-%20JupyterLab.png?generation=1758906523858560&amp;alt=media\" alt=\"\"></p>\n<ul>\n<li>We did not fix dispersion changes due to Jitter. It can still be significant. Here is the same planet where we plot the normalized variance of signal in one of FGS1 spatial axis. The bump in the middle means that more signal will be cropped from a narrow square.</li>\n</ul>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F75976%2F90992f5522784a9cdd749e78954af53c%2FScreenshot%202025-09-26%20at%2019-09-25%20exomod_531_f%20(auto-c%2013)%20-%20JupyterLab.png?generation=1758906657017925&amp;alt=media\" alt=\"\"></p>\n<p>-Light is not appearing as a nice ellipsis on the FGS1 sensor. Here is the log scaled average over all planets of FGS1 signal, after centering it:</p>\n<p><img src=\"https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F75976%2F6c4cf9b3028bead1d0d2373b1b4e2007%2FScreenshot%202025-09-26%20at%2019-11-39%20exomod_prepr%20(auto-c%2012)%20-%20JupyterLab.png?generation=1758906716843836&amp;alt=media\" alt=\"\"></p>\n<p>We masked the darker part of that average, rather than using a square crop of the center.</p>\n<p><strong>Main approach</strong></p>\n<p>We employed an iterative curve-fitting strategy to extract the planet-to-star radius ratio (Rp/Rs) from noisy transient light curve data. The procedure consisted of progressively refining the fit across different data representations:</p>\n<ol>\n<li>Phase detection – The orbital phase of the signal was identified to align the transit features. We used the largest gradient average over wavelengths, as in public notebooks.</li>\n<li>Baseline modeling – A degree 2 polynomial fit was applied to the out-of-transit “arms” of the light curve to model and remove baseline trends.</li>\n<li>Global transit fitting – The normalized light curve was fit using the batman transit model, yielding an initial estimate of Rp.</li>\n<li>FGS refinement – The fit was repeated on the FGS data</li>\n<li>Wavelength binning – The same fitting procedure was applied to 18 wavelength buckets, capturing wavelength-dependent variations.</li>\n<li>Wavelength binning with GP – For the bucketed data, a 2D polynomial plus Gaussian Process (GP) model was used to jointly capture systematic trends and correlated noise, refining the Rp estimate.</li>\n</ol>\n<p><strong>Postprocessing</strong></p>\n<ol>\n<li>combine predictions. We combined the results from the different curve fitting steps into 18 values for prediction and 283 values for sigma; those then go into a linear NN which weights the different sigma estimates and expands the 18 predictions back to 283 values</li>\n<li>train simple NN for calibrating predictions and sigma.. We use a NN because we used a GLLoss instead of the usual MSE for linear regression.</li>\n</ol>\n<pre><code> torch\n torch.nn  nn\n torch.nn.functional  F\n\n\n (nn.Module):\n     ():\n        ().__init__()\n        .pred_fc = nn.Linear(, , bias=)\n        .sigma_fc = nn.Linear(, , bias=)\n\n     ():\n        x = torch.log(pred).()\n        x = .pred_fc(x).squeeze(-)\n\n        y = torch.stack([sigma, s_std, var_l, s_std2, s_std3], ).()\n        y = torch.log(y)\n        y = .sigma_fc(y).squeeze(-)\n         x, y\n</code></pre>",
      "rawMarkdown": "Thanks to kaggle and everyone involved for hosting such an interesting competition. There was a lot to learn, and that's why we compete. \n\n**TLDR**\n\nOur solution is a a mix of transit light curve curve fitting using either batman + scipy-opitimize or pylightcurve-torch + gpytorch. We then trained a linear NN to calibrate predictions and estimate sigma values.\n\nWe only discovered Pylightcurve-torch and GPytorch a week before the end. We then sprinted to replicate and improve the batman pipeline with these. \n\n**Cross validation**\n\nCross validation was straightforward. We simply split the star into 4 folds. We evaluated the curve fitting steps without CV as there was no training, but for the NN postprocessing we did proper CV evaluation. Overall we had very good CV / LB correlation.\n\n**Data preprocessing**\n\nWe used @ilu000 preprocessing as a basis which has been shared here: https://www.kaggle.com/code/ilu000/ariel25-quick-data-prep-improved\n\nWe added a minor tweak for detecting cosmic rays, which was also shared in @jeroencottaar notebook https://www.kaggle.com/code/jeroencottaar/demonstrate-apparent-label-issue\n\nWe made the additional following changes. They yield over 0.005 score improvement when we implement them. \n- Inverted offset. The formula provided to use at the start of the competition was the formula used to encode the data. Given we had to decode, we have to invert both operations in the formula. Host only fixed one inversion during the competition. \n- The dark frame must be multiplied by the duration since sensor reset. We therefore updated the code this way:\n```python\n        # Step 4: dark current subtraction\n        if sensor == \"FGS1\":\n            dt = torch.ones(len(signal), device=\"cuda:0\") * 0.1\n            dt[1::2] += 0.3\n        elif sensor == \"AIRS-CH0\":\n            dt = torch.tensor(dt_airs).to(\"cuda:0\")\n            dt[1::2] += 0.2\n\n```\n- Jitter removal. We centered the signal by computing the center oliver spatial dimensions, then using scipy 2d interpolate to move the center to the center of the frame. Doing so we need to crop the outermost pixels.\n\nHere is an example of correcting AIRS center on the spatial axis. Orange is after correction.\n\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F75976%2F0db2d0135a74f942ed6687fd04eabceb%2FScreenshot%202025-09-26%20at%2019-05-26%20exomod_534_a%20(auto-c%2017)%20-%20JupyterLab.png?generation=1758906368284879&alt=media)\n\n- Some frames have very large center deviations. This is because of cosmic rays. We simply discard these frames and replace them by the one right before it. Here is an example of two cosmic rays effect on FGS1.\n\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F75976%2Faff24e5206a0ef5d63a49ca2d97cfb14%2FScreenshot%202025-09-26%20at%2019-08-18%20exomod_531_f%20(auto-c%2013)%20-%20JupyterLab.png?generation=1758906523858560&alt=media)\n\n- We did not fix dispersion changes due to Jitter. It can still be significant. Here is the same planet where we plot the normalized variance of signal in one of FGS1 spatial axis. The bump in the middle means that more signal will be cropped from a narrow square.\n\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F75976%2F90992f5522784a9cdd749e78954af53c%2FScreenshot%202025-09-26%20at%2019-09-25%20exomod_531_f%20(auto-c%2013)%20-%20JupyterLab.png?generation=1758906657017925&alt=media)\n\n-Light is not appearing as a nice ellipsis on the FGS1 sensor. Here is the log scaled average over all planets of FGS1 signal, after centering it:\n\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F75976%2F6c4cf9b3028bead1d0d2373b1b4e2007%2FScreenshot%202025-09-26%20at%2019-11-39%20exomod_prepr%20(auto-c%2012)%20-%20JupyterLab.png?generation=1758906716843836&alt=media)\n\nWe masked the darker part of that average, rather than using a square crop of the center.\n\n**Main approach**\n\nWe employed an iterative curve-fitting strategy to extract the planet-to-star radius ratio (Rp/Rs) from noisy transient light curve data. The procedure consisted of progressively refining the fit across different data representations:\n\n1. Phase detection – The orbital phase of the signal was identified to align the transit features. We used the largest gradient average over wavelengths, as in public notebooks.\n2. Baseline modeling – A degree 2 polynomial fit was applied to the out-of-transit “arms” of the light curve to model and remove baseline trends.\n3. Global transit fitting – The normalized light curve was fit using the batman transit model, yielding an initial estimate of Rp.\n4. FGS refinement – The fit was repeated on the FGS data\n5. Wavelength binning – The same fitting procedure was applied to 18 wavelength buckets, capturing wavelength-dependent variations.\n6. Wavelength binning with GP – For the bucketed data, a 2D polynomial plus Gaussian Process (GP) model was used to jointly capture systematic trends and correlated noise, refining the Rp estimate.\n\n**Postprocessing**\n\n1. combine predictions. We combined the results from the different curve fitting steps into 18 values for prediction and 283 values for sigma; those then go into a linear NN which weights the different sigma estimates and expands the 18 predictions back to 283 values\n2. train simple NN for calibrating predictions and sigma.. We use a NN because we used a GLLoss instead of the usual MSE for linear regression.\n\n\n```python\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\n\n\nclass Net(nn.Module):\n    def __init__(self, ):\n        super().__init__()\n        self.pred_fc = nn.Linear(18, 283, bias=True)\n        self.sigma_fc = nn.Linear(5, 1, bias=True)\n        \n    def forward(self, pred, sigma, s_std, var_l, s_std2, s_std3):\n        x = torch.log(pred).float()\n        x = self.pred_fc(x).squeeze(-1)\n\n        y = torch.stack([sigma, s_std, var_l, s_std2, s_std3], 2).float()\n        y = torch.log(y)\n        y = self.sigma_fc(y).squeeze(-1)\n        return x, y\n\n```\n\n",
      "votes": 23
    },
    {
      "id": 3294846,
      "postDate": "2025-09-26T22:47:44.250Z",
      "content": "<p>Thanks for posting this write-up! I have tried Pylightcurve-torch and batman as well, but my results are less than ideal. What specific method did you use to fit the light curve? Also, how did you get the limb darkening coefficients? Is that from the fitted parameters?</p>",
      "rawMarkdown": "Thanks for posting this write-up! I have tried Pylightcurve-torch and batman as well, but my results are less than ideal. What specific method did you use to fit the light curve? Also, how did you get the limb darkening coefficients? Is that from the fitted parameters?",
      "votes": 1,
      "replies": [
        {
          "id": 3294855,
          "postDate": "2025-09-26T23:49:22.170Z",
          "content": "<p>We used Nelder-Mead in scipy-optimize. To make it work you need to have good starting points. We estimated all parameters form observed data, like max dip to get a starting point for planet radius ratio.</p>",
          "rawMarkdown": "We used Nelder-Mead in scipy-optimize. To make it work you need to have good starting points. We estimated all parameters form observed data, like max dip to get a starting point for planet radius ratio."
        }
      ]
    },
    {
      "id": 3294946,
      "postDate": "2025-09-27T07:25:00.393Z",
      "content": "<p>Thank you very much for your solution! I also tried using Batman before, especially when it came to correcting the limb darken issue, but it didn't yield satisfactory results. Have you considered making the code public?</p>",
      "rawMarkdown": "Thank you very much for your solution! I also tried using Batman before, especially when it came to correcting the limb darken issue, but it didn't yield satisfactory results. Have you considered making the code public?",
      "replies": [
        {
          "id": 3294949,
          "postDate": "2025-09-27T07:40:07.067Z",
          "content": "<p>I am not sure. I see you aren't sure either given you have not open sourced your code.</p>",
          "rawMarkdown": "I am not sure. I see you aren't sure either given you have not open sourced your code.",
          "votes": 1,
          "replies": [
            {
              "id": 3294995,
              "postDate": "2025-09-27T10:19:36.253Z",
              "content": "<p>Yes. Agree and totally understand😄</p>",
              "rawMarkdown": "Yes. Agree and totally understand😄",
              "votes": 2
            }
          ]
        }
      ]
    },
    {
      "id": 3295103,
      "postDate": "2025-09-27T17:39:46.543Z",
      "content": "<p>Thank you so much for this solution </p>",
      "rawMarkdown": "Thank you so much for this solution "
    }
  ],
  "comments": [
    {
      "id": 3294846,
      "author_name": "mudesteven",
      "author_url": "",
      "post_date": "2025-09-26T22:47:44.250000",
      "content": "<p>Thanks for posting this write-up! I have tried Pylightcurve-torch and batman as well, but my results are less than ideal. What specific method did you use to fit the light curve? Also, how did you get the limb darkening coefficients? Is that from the fitted parameters?</p>",
      "votes": 1,
      "replies": [
        {
          "id": 3294855,
          "author_name": "CPMP",
          "author_url": "",
          "post_date": "2025-09-26T23:49:22.170000",
          "content": "<p>We used Nelder-Mead in scipy-optimize. To make it work you need to have good starting points. We estimated all parameters form observed data, like max dip to get a starting point for planet radius ratio.</p>",
          "votes": 0,
          "replies": []
        }
      ]
    },
    {
      "id": 3294946,
      "author_name": "Horikita Saku",
      "author_url": "",
      "post_date": "2025-09-27T07:25:00.393000",
      "content": "<p>Thank you very much for your solution! I also tried using Batman before, especially when it came to correcting the limb darken issue, but it didn't yield satisfactory results. Have you considered making the code public?</p>",
      "votes": 0,
      "replies": [
        {
          "id": 3294949,
          "author_name": "CPMP",
          "author_url": "",
          "post_date": "2025-09-27T07:40:07.067000",
          "content": "<p>I am not sure. I see you aren't sure either given you have not open sourced your code.</p>",
          "votes": 1,
          "replies": [
            {
              "id": 3294995,
              "author_name": "Horikita Saku",
              "author_url": "",
              "post_date": "2025-09-27T10:19:36.253000",
              "content": "<p>Yes. Agree and totally understand😄</p>",
              "votes": 2,
              "replies": []
            }
          ]
        }
      ]
    },
    {
      "id": 3295103,
      "author_name": "",
      "author_url": "",
      "post_date": "2025-09-27T17:39:46.543000",
      "content": "<p>Thank you so much for this solution </p>",
      "votes": 0,
      "replies": []
    }
  ],
  "raw_markdown_by_id": {
    "3294767": "Thanks to kaggle and everyone involved for hosting such an interesting competition. There was a lot to learn, and that's why we compete. \n\n**TLDR**\n\nOur solution is a a mix of transit light curve curve fitting using either batman + scipy-opitimize or pylightcurve-torch + gpytorch. We then trained a linear NN to calibrate predictions and estimate sigma values.\n\nWe only discovered Pylightcurve-torch and GPytorch a week before the end. We then sprinted to replicate and improve the batman pipeline with these. \n\n**Cross validation**\n\nCross validation was straightforward. We simply split the star into 4 folds. We evaluated the curve fitting steps without CV as there was no training, but for the NN postprocessing we did proper CV evaluation. Overall we had very good CV / LB correlation.\n\n**Data preprocessing**\n\nWe used @ilu000 preprocessing as a basis which has been shared here: https://www.kaggle.com/code/ilu000/ariel25-quick-data-prep-improved\n\nWe added a minor tweak for detecting cosmic rays, which was also shared in @jeroencottaar notebook https://www.kaggle.com/code/jeroencottaar/demonstrate-apparent-label-issue\n\nWe made the additional following changes. They yield over 0.005 score improvement when we implement them. \n- Inverted offset. The formula provided to use at the start of the competition was the formula used to encode the data. Given we had to decode, we have to invert both operations in the formula. Host only fixed one inversion during the competition. \n- The dark frame must be multiplied by the duration since sensor reset. We therefore updated the code this way:\n```python\n        # Step 4: dark current subtraction\n        if sensor == \"FGS1\":\n            dt = torch.ones(len(signal), device=\"cuda:0\") * 0.1\n            dt[1::2] += 0.3\n        elif sensor == \"AIRS-CH0\":\n            dt = torch.tensor(dt_airs).to(\"cuda:0\")\n            dt[1::2] += 0.2\n\n```\n- Jitter removal. We centered the signal by computing the center oliver spatial dimensions, then using scipy 2d interpolate to move the center to the center of the frame. Doing so we need to crop the outermost pixels.\n\nHere is an example of correcting AIRS center on the spatial axis. Orange is after correction.\n\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F75976%2F0db2d0135a74f942ed6687fd04eabceb%2FScreenshot%202025-09-26%20at%2019-05-26%20exomod_534_a%20(auto-c%2017)%20-%20JupyterLab.png?generation=1758906368284879&alt=media)\n\n- Some frames have very large center deviations. This is because of cosmic rays. We simply discard these frames and replace them by the one right before it. Here is an example of two cosmic rays effect on FGS1.\n\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F75976%2Faff24e5206a0ef5d63a49ca2d97cfb14%2FScreenshot%202025-09-26%20at%2019-08-18%20exomod_531_f%20(auto-c%2013)%20-%20JupyterLab.png?generation=1758906523858560&alt=media)\n\n- We did not fix dispersion changes due to Jitter. It can still be significant. Here is the same planet where we plot the normalized variance of signal in one of FGS1 spatial axis. The bump in the middle means that more signal will be cropped from a narrow square.\n\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F75976%2F90992f5522784a9cdd749e78954af53c%2FScreenshot%202025-09-26%20at%2019-09-25%20exomod_531_f%20(auto-c%2013)%20-%20JupyterLab.png?generation=1758906657017925&alt=media)\n\n-Light is not appearing as a nice ellipsis on the FGS1 sensor. Here is the log scaled average over all planets of FGS1 signal, after centering it:\n\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F75976%2F6c4cf9b3028bead1d0d2373b1b4e2007%2FScreenshot%202025-09-26%20at%2019-11-39%20exomod_prepr%20(auto-c%2012)%20-%20JupyterLab.png?generation=1758906716843836&alt=media)\n\nWe masked the darker part of that average, rather than using a square crop of the center.\n\n**Main approach**\n\nWe employed an iterative curve-fitting strategy to extract the planet-to-star radius ratio (Rp/Rs) from noisy transient light curve data. The procedure consisted of progressively refining the fit across different data representations:\n\n1. Phase detection – The orbital phase of the signal was identified to align the transit features. We used the largest gradient average over wavelengths, as in public notebooks.\n2. Baseline modeling – A degree 2 polynomial fit was applied to the out-of-transit “arms” of the light curve to model and remove baseline trends.\n3. Global transit fitting – The normalized light curve was fit using the batman transit model, yielding an initial estimate of Rp.\n4. FGS refinement – The fit was repeated on the FGS data\n5. Wavelength binning – The same fitting procedure was applied to 18 wavelength buckets, capturing wavelength-dependent variations.\n6. Wavelength binning with GP – For the bucketed data, a 2D polynomial plus Gaussian Process (GP) model was used to jointly capture systematic trends and correlated noise, refining the Rp estimate.\n\n**Postprocessing**\n\n1. combine predictions. We combined the results from the different curve fitting steps into 18 values for prediction and 283 values for sigma; those then go into a linear NN which weights the different sigma estimates and expands the 18 predictions back to 283 values\n2. train simple NN for calibrating predictions and sigma.. We use a NN because we used a GLLoss instead of the usual MSE for linear regression.\n\n\n```python\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\n\n\nclass Net(nn.Module):\n    def __init__(self, ):\n        super().__init__()\n        self.pred_fc = nn.Linear(18, 283, bias=True)\n        self.sigma_fc = nn.Linear(5, 1, bias=True)\n        \n    def forward(self, pred, sigma, s_std, var_l, s_std2, s_std3):\n        x = torch.log(pred).float()\n        x = self.pred_fc(x).squeeze(-1)\n\n        y = torch.stack([sigma, s_std, var_l, s_std2, s_std3], 2).float()\n        y = torch.log(y)\n        y = self.sigma_fc(y).squeeze(-1)\n        return x, y\n\n```\n\n",
    "3294846": "Thanks for posting this write-up! I have tried Pylightcurve-torch and batman as well, but my results are less than ideal. What specific method did you use to fit the light curve? Also, how did you get the limb darkening coefficients? Is that from the fitted parameters?",
    "3294946": "Thank you very much for your solution! I also tried using Batman before, especially when it came to correcting the limb darken issue, but it didn't yield satisfactory results. Have you considered making the code public?",
    "3295103": "Thank you so much for this solution "
  }
}