{
  "id": 403334,
  "title": "22th solution: fine tune pre-trained GraphNet combined with plane fit by BDT  ",
  "url": "/competitions/icecube-neutrinos-in-deep-ice/discussion/403334",
  "author_name": "Brown Zerg",
  "post_date": "2023-04-22T14:20:56.593000",
  "votes": 3,
  "comment_count": 0,
  "views": 0,
  "content": "<p>First of all, as a former HEPer, I am really excited to see HEP-related challenges on Kaggle. Thank you, IceCube and Kaggle, for hosting this competition!</p>\n<p>It's impossible to achieve great results without the sharing of ideas and code within the community. As a newcomer, I have learned a lot. In particular, I would like to express my gratitude to <a href=\"https://www.kaggle.com/rasmusrse\" target=\"_blank\">@rasmusrse</a> for sharing the <a href=\"https://www.kaggle.com/code/rasmusrse/graphnet-example\" target=\"_blank\">GraphNet example</a> and to <a href=\"https://www.kaggle.com/amoshuangyc\" target=\"_blank\">@amoshuangyc</a> for the  <a href=\"https://www.kaggle.com/code/amoshuangyc/icecube-gnn-baseline-rewrite\" target=\"_blank\">rewriting</a>, which made it even more accessible.</p>\n<h2>Solution</h2>\n<p>The notebook that I submitted can be found <a href=\"https://www.kaggle.com/code/brownzerg/22th-fined-tuned-gnn-comb-with-plane-fit-by-bdt\" target=\"_blank\">here</a>.</p>\n<h3>Fine-tune of pre-trained model</h3>\n<p>The DynEdge architecture and the loss function seem to be pretty solid and optimal for the problem, although further information may be required for real physics analysis to make it more powerful. This information may include the position of interaction vertices, the type of events, etc. In fact, the GraphNet is capable of doing all that! Meanwhile, I'm really curious to see other architectures/loss functions that have been developed during the competition!</p>\n<p>To learn the architecture, I studied the published paper and code on GitHub. When I came across the pre-trained model shared at here, I decided to start from there. With the help of the aforementioned re-write of the GraphNet DynEdge model and some minor modifications, I was able to load the pre-trained model. My fine-tuning scheme is as follows:</p>\n<p><strong>[Step 1]</strong> I used batch 101 to 600 (61 to 80 for validation) and tuned the model with a piecewise-linear learning rate. In the first half epoch, the learning rate was increased from 1e-5 to 1e-3 (warming up), and then until the 10th epoch, it was decreased from 1e-3 to 1e-6. This resulted in an error of around 0.996.</p>\n<p>It's worth mentioning that I used a maximum of 200 pulses in each event partially due to computational resource constraints. These pulses were not randomly selected, but priority was given to those that were not labelled as auxiliary. Here is my implementation of pulse down-sampling:</p>\n<pre><code>if len(df) &gt; self.max_pulses:\n    df_pass = df[~df.auxiliary]\n    df_fail = df[df.auxiliary]\n    if len(df_pass) &gt;= self.max_pulses:\n        df = df_pass.sample(self.max_pulses)\n    else:\n        df_fail = df_fail.sample(self.max_pulses - len(df_pass))\n        df = pd.concat([df_fail, df_pass])\n</code></pre>\n<p>In a training with a smaller amount of data, I found that this technique helps the GraphNet to converge a bit faster.</p>\n<p><strong>[Step 2]</strong> I used batch 101, 103, …, 599 (a total of 250 batches due to starting quite late) and added additional noise to the times and charges to help with the training. In PyTorch, the implementation looks something like this:</p>\n<pre><code>from torch.distributions import Gamma, Normal\n...\ndist_normal = Normal(0, 1.2)\ndist_gamma = Gamma(c * 10, 10)\nt += dist_normal.sample()\nc = dist_gamma.sample()\n</code></pre>\n<p>For the additional noise, I chose a Gaussian distribution for the time parameter, considering a time resolution of 1.2 ns as stated in some literature. I used a Gamma distribution for the charge parameter, taking into account the Poisson nature of the photoelectrics. The choice of parameters for the Gamma distribution was somewhat random, as I was trying to control the width of the distribution. I acknowledge that this could potentially be incorrect, but it resulted in a further improvement of 0.002.<br>\nI trained this step on top of the model trained in step (1) with a learning rate of 1e-5, which linearly decreased to 1e-8 after 5 epochs.</p>\n<h3>Plane fit + BDT</h3>\n<p>At the same time, I wanted to explore a simple solution or obtain additional information while the training was running. I noticed that the pulse points could be used to fit a plane in 3-D by solving a least squares problem. I found that the average angle between the plane and the true direction of the neutrino was relatively small, around 16 degrees. This led me to consider projecting the DynEdge prediction onto the plane as a possible way to improve the results.</p>\n<p>About half of the projected predictions were better than the DynEdge-only predictions. If we could determine which prediction was better, we could achieve a final error of approximately 0.91. Even if we could only determine this with an accuracy of 80%, we could still achieve an error below 0.95. However, since we cannot determine this accurately, I trained a boosted decision tree (BDT) to distinguish for each event whether the projected or the original prediction was better. The input variables for the BDT are defined in block (11) of my notebook, with zenith, kappa, dt, ez, and xe providing the greatest discrimination power. The final accuracy of the BDT to determine which cases the plane-fit projection was better was only about 61%, which is not great. By tuning the cut threshold of the BDT output score, the final error was improved by approximately 0.001.</p>\n<h2>Conclusion</h2>\n<p>Overall, I think the solution is relatively straightforward and cost-effective, comprising only a GNN, a simple least square fit, and a BDT. Eventually, I have some additional thoughts to share:</p>\n<ol>\n<li><p>The second stage training with noise data can be further optimised. The distribution can be tuned and the batches and epoches for training may not be enough.</p></li>\n<li><p>One could categorize events based on the GraphNet output <code>kappa</code>. For instance, one might consider dividing events into those with low (kappa&lt;0.5) and high (kappa&gt;=0.5) values, but further optimization would be needed to determine the impact of this categorization. From what I understand, kappa represents the difficulty of predicting a given event. The distribution of kappa is intriguing, with three peaks: one below 0.5, one near 1, and one near 2. Events with kappa values below 0.5 tend to have errors around 1.48 (close to pi/2), which is almost the worst one can get, while events with higher kappa values have pretty low errors. I noticed that events with kappa values below 0.5 tend to have a center of charge inside the dust layer and above, where there is no DeepCore, quantum efficiency is worse and ice absorption and scattering are larger. Therefore, I surmise that the three peaks might correspond to:<br>\na. poor detection or cluster/double-bang events<br>\nb. good detection or poor detection + muon track<br>\nc. excellent detection or good detection + muon track</p></li>\n<li><p>The better the DynEdge prediction is, the less the plane fit contributes. It could be interesting to explore how much the plane fit helps with low kappa values. However, the plane fit + BDT contribution have only a marginal impact on the final error.</p></li>\n</ol>\n<p>Cheers,<br>\nBlue Zenith</p>",
  "messages": [
    {
      "id": 2230584,
      "postDate": "2023-04-22T14:20:56.593Z",
      "content": "<p>First of all, as a former HEPer, I am really excited to see HEP-related challenges on Kaggle. Thank you, IceCube and Kaggle, for hosting this competition!</p>\n<p>It's impossible to achieve great results without the sharing of ideas and code within the community. As a newcomer, I have learned a lot. In particular, I would like to express my gratitude to <a href=\"https://www.kaggle.com/rasmusrse\" target=\"_blank\">@rasmusrse</a> for sharing the <a href=\"https://www.kaggle.com/code/rasmusrse/graphnet-example\" target=\"_blank\">GraphNet example</a> and to <a href=\"https://www.kaggle.com/amoshuangyc\" target=\"_blank\">@amoshuangyc</a> for the  <a href=\"https://www.kaggle.com/code/amoshuangyc/icecube-gnn-baseline-rewrite\" target=\"_blank\">rewriting</a>, which made it even more accessible.</p>\n<h2>Solution</h2>\n<p>The notebook that I submitted can be found <a href=\"https://www.kaggle.com/code/brownzerg/22th-fined-tuned-gnn-comb-with-plane-fit-by-bdt\" target=\"_blank\">here</a>.</p>\n<h3>Fine-tune of pre-trained model</h3>\n<p>The DynEdge architecture and the loss function seem to be pretty solid and optimal for the problem, although further information may be required for real physics analysis to make it more powerful. This information may include the position of interaction vertices, the type of events, etc. In fact, the GraphNet is capable of doing all that! Meanwhile, I'm really curious to see other architectures/loss functions that have been developed during the competition!</p>\n<p>To learn the architecture, I studied the published paper and code on GitHub. When I came across the pre-trained model shared at here, I decided to start from there. With the help of the aforementioned re-write of the GraphNet DynEdge model and some minor modifications, I was able to load the pre-trained model. My fine-tuning scheme is as follows:</p>\n<p><strong>[Step 1]</strong> I used batch 101 to 600 (61 to 80 for validation) and tuned the model with a piecewise-linear learning rate. In the first half epoch, the learning rate was increased from 1e-5 to 1e-3 (warming up), and then until the 10th epoch, it was decreased from 1e-3 to 1e-6. This resulted in an error of around 0.996.</p>\n<p>It's worth mentioning that I used a maximum of 200 pulses in each event partially due to computational resource constraints. These pulses were not randomly selected, but priority was given to those that were not labelled as auxiliary. Here is my implementation of pulse down-sampling:</p>\n<pre><code>if len(df) &gt; self.max_pulses:\n    df_pass = df[~df.auxiliary]\n    df_fail = df[df.auxiliary]\n    if len(df_pass) &gt;= self.max_pulses:\n        df = df_pass.sample(self.max_pulses)\n    else:\n        df_fail = df_fail.sample(self.max_pulses - len(df_pass))\n        df = pd.concat([df_fail, df_pass])\n</code></pre>\n<p>In a training with a smaller amount of data, I found that this technique helps the GraphNet to converge a bit faster.</p>\n<p><strong>[Step 2]</strong> I used batch 101, 103, …, 599 (a total of 250 batches due to starting quite late) and added additional noise to the times and charges to help with the training. In PyTorch, the implementation looks something like this:</p>\n<pre><code>from torch.distributions import Gamma, Normal\n...\ndist_normal = Normal(0, 1.2)\ndist_gamma = Gamma(c * 10, 10)\nt += dist_normal.sample()\nc = dist_gamma.sample()\n</code></pre>\n<p>For the additional noise, I chose a Gaussian distribution for the time parameter, considering a time resolution of 1.2 ns as stated in some literature. I used a Gamma distribution for the charge parameter, taking into account the Poisson nature of the photoelectrics. The choice of parameters for the Gamma distribution was somewhat random, as I was trying to control the width of the distribution. I acknowledge that this could potentially be incorrect, but it resulted in a further improvement of 0.002.<br>\nI trained this step on top of the model trained in step (1) with a learning rate of 1e-5, which linearly decreased to 1e-8 after 5 epochs.</p>\n<h3>Plane fit + BDT</h3>\n<p>At the same time, I wanted to explore a simple solution or obtain additional information while the training was running. I noticed that the pulse points could be used to fit a plane in 3-D by solving a least squares problem. I found that the average angle between the plane and the true direction of the neutrino was relatively small, around 16 degrees. This led me to consider projecting the DynEdge prediction onto the plane as a possible way to improve the results.</p>\n<p>About half of the projected predictions were better than the DynEdge-only predictions. If we could determine which prediction was better, we could achieve a final error of approximately 0.91. Even if we could only determine this with an accuracy of 80%, we could still achieve an error below 0.95. However, since we cannot determine this accurately, I trained a boosted decision tree (BDT) to distinguish for each event whether the projected or the original prediction was better. The input variables for the BDT are defined in block (11) of my notebook, with zenith, kappa, dt, ez, and xe providing the greatest discrimination power. The final accuracy of the BDT to determine which cases the plane-fit projection was better was only about 61%, which is not great. By tuning the cut threshold of the BDT output score, the final error was improved by approximately 0.001.</p>\n<h2>Conclusion</h2>\n<p>Overall, I think the solution is relatively straightforward and cost-effective, comprising only a GNN, a simple least square fit, and a BDT. Eventually, I have some additional thoughts to share:</p>\n<ol>\n<li><p>The second stage training with noise data can be further optimised. The distribution can be tuned and the batches and epoches for training may not be enough.</p></li>\n<li><p>One could categorize events based on the GraphNet output <code>kappa</code>. For instance, one might consider dividing events into those with low (kappa&lt;0.5) and high (kappa&gt;=0.5) values, but further optimization would be needed to determine the impact of this categorization. From what I understand, kappa represents the difficulty of predicting a given event. The distribution of kappa is intriguing, with three peaks: one below 0.5, one near 1, and one near 2. Events with kappa values below 0.5 tend to have errors around 1.48 (close to pi/2), which is almost the worst one can get, while events with higher kappa values have pretty low errors. I noticed that events with kappa values below 0.5 tend to have a center of charge inside the dust layer and above, where there is no DeepCore, quantum efficiency is worse and ice absorption and scattering are larger. Therefore, I surmise that the three peaks might correspond to:<br>\na. poor detection or cluster/double-bang events<br>\nb. good detection or poor detection + muon track<br>\nc. excellent detection or good detection + muon track</p></li>\n<li><p>The better the DynEdge prediction is, the less the plane fit contributes. It could be interesting to explore how much the plane fit helps with low kappa values. However, the plane fit + BDT contribution have only a marginal impact on the final error.</p></li>\n</ol>\n<p>Cheers,<br>\nBlue Zenith</p>",
      "rawMarkdown": "First of all, as a former HEPer, I am really excited to see HEP-related challenges on Kaggle. Thank you, IceCube and Kaggle, for hosting this competition!\n\nIt's impossible to achieve great results without the sharing of ideas and code within the community. As a newcomer, I have learned a lot. In particular, I would like to express my gratitude to @rasmusrse for sharing the [GraphNet example](https://www.kaggle.com/code/rasmusrse/graphnet-example) and to @amoshuangyc for the  [rewriting](https://www.kaggle.com/code/amoshuangyc/icecube-gnn-baseline-rewrite), which made it even more accessible.\n\n## Solution\nThe notebook that I submitted can be found [here](https://www.kaggle.com/code/brownzerg/22th-fined-tuned-gnn-comb-with-plane-fit-by-bdt).\n\n### Fine-tune of pre-trained model\nThe DynEdge architecture and the loss function seem to be pretty solid and optimal for the problem, although further information may be required for real physics analysis to make it more powerful. This information may include the position of interaction vertices, the type of events, etc. In fact, the GraphNet is capable of doing all that! Meanwhile, I'm really curious to see other architectures/loss functions that have been developed during the competition!\n\nTo learn the architecture, I studied the published paper and code on GitHub. When I came across the pre-trained model shared at here, I decided to start from there. With the help of the aforementioned re-write of the GraphNet DynEdge model and some minor modifications, I was able to load the pre-trained model. My fine-tuning scheme is as follows:\n\n**[Step 1]** I used batch 101 to 600 (61 to 80 for validation) and tuned the model with a piecewise-linear learning rate. In the first half epoch, the learning rate was increased from 1e-5 to 1e-3 (warming up), and then until the 10th epoch, it was decreased from 1e-3 to 1e-6. This resulted in an error of around 0.996.\n\nIt's worth mentioning that I used a maximum of 200 pulses in each event partially due to computational resource constraints. These pulses were not randomly selected, but priority was given to those that were not labelled as auxiliary. Here is my implementation of pulse down-sampling:\n\n```\nif len(df) > self.max_pulses:\n    df_pass = df[~df.auxiliary]\n    df_fail = df[df.auxiliary]\n    if len(df_pass) >= self.max_pulses:\n        df = df_pass.sample(self.max_pulses)\n    else:\n        df_fail = df_fail.sample(self.max_pulses - len(df_pass))\n        df = pd.concat([df_fail, df_pass])\n```In a training with a smaller amount of data, I found that this technique helps the GraphNet to converge a bit faster.\n\n**[Step 2]** I used batch 101, 103, ..., 599 (a total of 250 batches due to starting quite late) and added additional noise to the times and charges to help with the training. In PyTorch, the implementation looks something like this:\n\n```\nfrom torch.distributions import Gamma, Normal\n...\ndist_normal = Normal(0, 1.2)\ndist_gamma = Gamma(c * 10, 10)\nt += dist_normal.sample()\nc = dist_gamma.sample()\n```\n\nFor the additional noise, I chose a Gaussian distribution for the time parameter, considering a time resolution of 1.2 ns as stated in some literature. I used a Gamma distribution for the charge parameter, taking into account the Poisson nature of the photoelectrics. The choice of parameters for the Gamma distribution was somewhat random, as I was trying to control the width of the distribution. I acknowledge that this could potentially be incorrect, but it resulted in a further improvement of 0.002.\nI trained this step on top of the model trained in step (1) with a learning rate of 1e-5, which linearly decreased to 1e-8 after 5 epochs.\n\n### Plane fit + BDT\n\nAt the same time, I wanted to explore a simple solution or obtain additional information while the training was running. I noticed that the pulse points could be used to fit a plane in 3-D by solving a least squares problem. I found that the average angle between the plane and the true direction of the neutrino was relatively small, around 16 degrees. This led me to consider projecting the DynEdge prediction onto the plane as a possible way to improve the results.\n\nAbout half of the projected predictions were better than the DynEdge-only predictions. If we could determine which prediction was better, we could achieve a final error of approximately 0.91. Even if we could only determine this with an accuracy of 80%, we could still achieve an error below 0.95. However, since we cannot determine this accurately, I trained a boosted decision tree (BDT) to distinguish for each event whether the projected or the original prediction was better. The input variables for the BDT are defined in block (11) of my notebook, with zenith, kappa, dt, ez, and xe providing the greatest discrimination power. The final accuracy of the BDT to determine which cases the plane-fit projection was better was only about 61%, which is not great. By tuning the cut threshold of the BDT output score, the final error was improved by approximately 0.001.\n\n## Conclusion\n\nOverall, I think the solution is relatively straightforward and cost-effective, comprising only a GNN, a simple least square fit, and a BDT. Eventually, I have some additional thoughts to share:\n\n1. The second stage training with noise data can be further optimised. The distribution can be tuned and the batches and epoches for training may not be enough.\n\n2. One could categorize events based on the GraphNet output `kappa`. For instance, one might consider dividing events into those with low (kappa<0.5) and high (kappa>=0.5) values, but further optimization would be needed to determine the impact of this categorization. From what I understand, kappa represents the difficulty of predicting a given event. The distribution of kappa is intriguing, with three peaks: one below 0.5, one near 1, and one near 2. Events with kappa values below 0.5 tend to have errors around 1.48 (close to pi/2), which is almost the worst one can get, while events with higher kappa values have pretty low errors. I noticed that events with kappa values below 0.5 tend to have a center of charge inside the dust layer and above, where there is no DeepCore, quantum efficiency is worse and ice absorption and scattering are larger. Therefore, I surmise that the three peaks might correspond to:\na. poor detection or cluster/double-bang events\nb. good detection or poor detection + muon track\nc. excellent detection or good detection + muon track\n\n3. The better the DynEdge prediction is, the less the plane fit contributes. It could be interesting to explore how much the plane fit helps with low kappa values. However, the plane fit + BDT contribution have only a marginal impact on the final error.\n\nCheers,\nBlue Zenith",
      "votes": 3
    }
  ],
  "comments": [],
  "raw_markdown_by_id": {
    "2230584": "First of all, as a former HEPer, I am really excited to see HEP-related challenges on Kaggle. Thank you, IceCube and Kaggle, for hosting this competition!\n\nIt's impossible to achieve great results without the sharing of ideas and code within the community. As a newcomer, I have learned a lot. In particular, I would like to express my gratitude to @rasmusrse for sharing the [GraphNet example](https://www.kaggle.com/code/rasmusrse/graphnet-example) and to @amoshuangyc for the  [rewriting](https://www.kaggle.com/code/amoshuangyc/icecube-gnn-baseline-rewrite), which made it even more accessible.\n\n## Solution\nThe notebook that I submitted can be found [here](https://www.kaggle.com/code/brownzerg/22th-fined-tuned-gnn-comb-with-plane-fit-by-bdt).\n\n### Fine-tune of pre-trained model\nThe DynEdge architecture and the loss function seem to be pretty solid and optimal for the problem, although further information may be required for real physics analysis to make it more powerful. This information may include the position of interaction vertices, the type of events, etc. In fact, the GraphNet is capable of doing all that! Meanwhile, I'm really curious to see other architectures/loss functions that have been developed during the competition!\n\nTo learn the architecture, I studied the published paper and code on GitHub. When I came across the pre-trained model shared at here, I decided to start from there. With the help of the aforementioned re-write of the GraphNet DynEdge model and some minor modifications, I was able to load the pre-trained model. My fine-tuning scheme is as follows:\n\n**[Step 1]** I used batch 101 to 600 (61 to 80 for validation) and tuned the model with a piecewise-linear learning rate. In the first half epoch, the learning rate was increased from 1e-5 to 1e-3 (warming up), and then until the 10th epoch, it was decreased from 1e-3 to 1e-6. This resulted in an error of around 0.996.\n\nIt's worth mentioning that I used a maximum of 200 pulses in each event partially due to computational resource constraints. These pulses were not randomly selected, but priority was given to those that were not labelled as auxiliary. Here is my implementation of pulse down-sampling:\n\n```\nif len(df) > self.max_pulses:\n    df_pass = df[~df.auxiliary]\n    df_fail = df[df.auxiliary]\n    if len(df_pass) >= self.max_pulses:\n        df = df_pass.sample(self.max_pulses)\n    else:\n        df_fail = df_fail.sample(self.max_pulses - len(df_pass))\n        df = pd.concat([df_fail, df_pass])\n```In a training with a smaller amount of data, I found that this technique helps the GraphNet to converge a bit faster.\n\n**[Step 2]** I used batch 101, 103, ..., 599 (a total of 250 batches due to starting quite late) and added additional noise to the times and charges to help with the training. In PyTorch, the implementation looks something like this:\n\n```\nfrom torch.distributions import Gamma, Normal\n...\ndist_normal = Normal(0, 1.2)\ndist_gamma = Gamma(c * 10, 10)\nt += dist_normal.sample()\nc = dist_gamma.sample()\n```\n\nFor the additional noise, I chose a Gaussian distribution for the time parameter, considering a time resolution of 1.2 ns as stated in some literature. I used a Gamma distribution for the charge parameter, taking into account the Poisson nature of the photoelectrics. The choice of parameters for the Gamma distribution was somewhat random, as I was trying to control the width of the distribution. I acknowledge that this could potentially be incorrect, but it resulted in a further improvement of 0.002.\nI trained this step on top of the model trained in step (1) with a learning rate of 1e-5, which linearly decreased to 1e-8 after 5 epochs.\n\n### Plane fit + BDT\n\nAt the same time, I wanted to explore a simple solution or obtain additional information while the training was running. I noticed that the pulse points could be used to fit a plane in 3-D by solving a least squares problem. I found that the average angle between the plane and the true direction of the neutrino was relatively small, around 16 degrees. This led me to consider projecting the DynEdge prediction onto the plane as a possible way to improve the results.\n\nAbout half of the projected predictions were better than the DynEdge-only predictions. If we could determine which prediction was better, we could achieve a final error of approximately 0.91. Even if we could only determine this with an accuracy of 80%, we could still achieve an error below 0.95. However, since we cannot determine this accurately, I trained a boosted decision tree (BDT) to distinguish for each event whether the projected or the original prediction was better. The input variables for the BDT are defined in block (11) of my notebook, with zenith, kappa, dt, ez, and xe providing the greatest discrimination power. The final accuracy of the BDT to determine which cases the plane-fit projection was better was only about 61%, which is not great. By tuning the cut threshold of the BDT output score, the final error was improved by approximately 0.001.\n\n## Conclusion\n\nOverall, I think the solution is relatively straightforward and cost-effective, comprising only a GNN, a simple least square fit, and a BDT. Eventually, I have some additional thoughts to share:\n\n1. The second stage training with noise data can be further optimised. The distribution can be tuned and the batches and epoches for training may not be enough.\n\n2. One could categorize events based on the GraphNet output `kappa`. For instance, one might consider dividing events into those with low (kappa<0.5) and high (kappa>=0.5) values, but further optimization would be needed to determine the impact of this categorization. From what I understand, kappa represents the difficulty of predicting a given event. The distribution of kappa is intriguing, with three peaks: one below 0.5, one near 1, and one near 2. Events with kappa values below 0.5 tend to have errors around 1.48 (close to pi/2), which is almost the worst one can get, while events with higher kappa values have pretty low errors. I noticed that events with kappa values below 0.5 tend to have a center of charge inside the dust layer and above, where there is no DeepCore, quantum efficiency is worse and ice absorption and scattering are larger. Therefore, I surmise that the three peaks might correspond to:\na. poor detection or cluster/double-bang events\nb. good detection or poor detection + muon track\nc. excellent detection or good detection + muon track\n\n3. The better the DynEdge prediction is, the less the plane fit contributes. It could be interesting to explore how much the plane fit helps with low kappa values. However, the plane fit + BDT contribution have only a marginal impact on the final error.\n\nCheers,\nBlue Zenith"
  }
}