• Home
  • Team
  • Science
  • Publications
  • Visual Stories
    • CH4
    • Clouds
    • Surface Spectra
  • News

Surface Spectra

While the core mission science focuses on observations of atmospheric composition, the mission will also observe surface properties at unmatched spectral resolutions.

Carbon-I mission is designed as a wide-swath mapping mission, collecting shortwave infrared imaging spectroscopy data. This will provide new insights into a wide range of areas including critical mineral distributions, vegetation stress, and pollution. Carbon-I’s higher spectral resolution in the shortwave infrared compared to existing missions (e.g., EMIT) allows clearer discrimination of minerals such as lithium-bearing hectorite from similar materials, offering significant value for scientific analysis and potential resource exploration. The Carbon-I team has taken surface reflectance measurements with the Carbon-I Demonstration Unit in the laboratory - check them out below with this tool which allows you to compare these highly resolved spectral data with coarser spectral resolutions to see the power of Carbon-I’s surface spectra.

import { rangeSlider as rangeSlider } from '@mootari/range-slider'
rawData =  FileAttachment("data/rfl_lowpass_shaped_v2.csv").csv({typed: true});
fmt = d3.format(".2f");

viewof slider = rangeSlider({
  min: d3.extent(rawData, d => d.wavelength)[0],
  max: d3.extent(rawData, d => d.wavelength)[1],
  step: 0.7,
  format: d => `${fmt(d)} nm`,
  title: 'Select a range',
})

minWL = slider[0];
maxWL = slider[1];
viewof normalize = Inputs.toggle({
  label: "Normalize 0–1 (per sample)",
  value: false
})
categoryLookup = new Map([
  ["Hectorite", {category: "Minerals", color: "#4daf4a"}],  
  ["Magnesite", {category: "Minerals", color: "#ff7f00"}], 
  ["Saponite", {category: "Minerals", color: "#a65628"}], 
  ["Sepiolite", {category: "Minerals", color: "#e377c2"}], 
  ["Vermiculite", {category: "Minerals", color: "#7f7f7f"}],  

  ["Oak leaf, new growth", {category: "Vegetation", color: "#3a8040"}],  
  ["Oak leaf, one year old", {category: "Vegetation", color: "#ffdd57"}],
  ["Oak leaf, scenesced", {category: "Vegetation", color: "#1b9e77"}],
  ["Maple Leaf", {category: "Vegetation", color: "#7570b3"}],

  ["HDPE bottle",  {category: "Plastics", color: "#1f78b4"}], 
  ["HDPE film", {category: "Plastics", color: "#33a02c"}], 
  ["PVC", {category: "Plastics", color: "#6a3d9a"}],
  ["Polysterene", {category: "Plastics", color: "#b15928"}]   
]);
samples = d3.sort(d3.group(rawData, d => d.sample).keys());

categorizedData = rawData.map(d => {
  const info = categoryLookup.get(d.sample)
  return {
    ...d,
    category : info.category,  
    color : info.color               
  };
});

// wrapping data in set removes duplicates
categories = Array.from(new Set(categorizedData.map(d => d.category)));
viewof categorySel = Inputs.checkbox(categories, {
  label: "Categories", 
  value: ["Plastics"]}
);
sampleOptions = Array.from(categoryLookup.keys()).filter(
  s => samples.includes(s) && categorySel.includes(categoryLookup.get(s).category)
);
viewof sampleSel = Inputs.checkbox(sampleOptions, {
  value: sampleOptions 
});
viewof samplePanel = categorySel.length
  ? html`<details class="sample-drop" open>
           <summary style="color:#000;">Samples</summary>
           ${viewof sampleSel}
         </details>`
  : html``;
viewof resolutionSel = Inputs.range([0.7, 15.4], {step: 0.7, value: 0.7, label: "Resolution (nm)"})
viewof windowSel = Inputs.range([1, 15], {step: 1, value: 1, label: "Spectral Averaging Window"})
filteredData = categorizedData.filter(
  d => d.wavelength >= minWL && 
  d.wavelength <= maxWL && 
  categorySel.includes(d.category) &&
  sampleSel.includes(d.sample)
);
{
  if (filteredData.length < 1) {
    return html`<div style="color:#666;">No active selections to plot.</div>`;
  }

  const rowsBySample = d3.group(filteredData, d => d.sample);  // Map<sample, rows[]>
  const adjustedData = [];
  const resolutionSize = Math.max(1, Math.round(resolutionSel / 0.7)); 

  for (const [sample, rows] of rowsBySample) {
    let adjusted = rows.map(r => r.reflectance);

    if (windowSel > 1){
      adjusted = d3.blur(adjusted, windowSel);
    } 

    let ymin, ymax, denom;
    if (normalize) {
      ymin = d3.min(adjusted);
      ymax = d3.max(adjusted);
      denom = ymax - ymin; 
    }

    for (let i = 0; i < adjusted.length; i += resolutionSize) {
      let y = adjusted[i];
      if (normalize){
        y = denom > 0 ? (adjusted[i] - ymin) / denom : 0 //avoid divide by 0
      }
      
      adjustedData.push({
        wavelength: rows[i].wavelength,
        reflectance: y,
        sample
      });
    }
  }

  const activeSamples = Array.from(new Set(filteredData.map(d => d.sample)));
  const plotWidth = Math.min(width, 1500);
  const fontPx = Math.max(10, Math.round(plotWidth / 70));
  const rotate = plotWidth < 650 ? -45 : 0;

  const tickStart = Math.ceil(minWL / 30) * 30;
  const tickEnd = Math.floor(maxWL / 30) * 30;

  const yLabel = normalize ? "Normalized reflectance" : "Reflectance";
  const maxY = normalize ? 1.03 : d3.max(adjustedData, d => d.reflectance) * 1.03;

  const fmtVal = normalize ? d3.format(".3f") : d3.format(".4f");

  return Plot.plot({
    width: plotWidth,
    height: Math.round(plotWidth * (13/20)),
    marginRight: 150,
    marginLeft: 55,   
    marginTop: 40,   
    marginBottom: 50,

    x: {
      label: "Wavelength (nm)",
      grid: true,
      tickPadding: 10,
      tickRotate: rotate,
      ticks: d3.range(tickStart, tickEnd + 1, 30),
      tickFormat: d => d.toFixed(2),
    },
    y: {
      label: yLabel,
      grid: true,
      tickPadding: 10,
      domain: [0, maxY]
    },
    color: {
      legend: true,
      domain: activeSamples,
      range: activeSamples.map(s => categoryLookup.get(s).color),
      inset: { 
        anchor: "top-right", 
        inset: 10, 
        padding: 5 
      }
    },
    style: { 
      fontSize: `${fontPx}px`, 
      "--plot-font-size": `${fontPx}px` 
    },

    marks: [
      Plot.line(adjustedData, {
        x: "wavelength",
        y: "reflectance",
        stroke: "sample",
        title: "sample",
        strokeWidth: 4
      }),
      Plot.dot(adjustedData, Plot.pointerX({
        x: "wavelength",
        y: "reflectance",
        fill: "sample",
        stroke: "white",
        marker: "circle",
        strokeWidth: 2.5,
        r: 5
      })),
      Plot.text(adjustedData, Plot.pointerX({
        px: "wavelength",
        py: "reflectance",
        frameAnchor: "top-left",
        fontVariant: "tabular-nums",
        fontSize: fontPx * 0.8,
        text: d => `${d.sample}   λ ${fmt(d.wavelength)} nm    ${fmtVal(d.reflectance)}`
      })),
      Plot.text(adjustedData, Plot.selectLast({
        x: "wavelength",
        y: "reflectance",
        z: "sample",
        text: "sample",
        textAnchor: "start",
        fontSize: fontPx * 0.7,
        dx: 3
      }))
    ]
  });
}
Source Code
---
comments: false
page-layout: custom
format:
  html:
    margin-top: 0em
    margin-bottom: 0em
    minimal: true
    smooth-scroll: true
    fig-responsive: true
    toc: false
    echo: false
    keep-hidden: true
    code-tools: true
---

:::: {.hero-small .hero-gradient .hero-clouds}
::: {.container}
# **Surface Spectra**
:::
::::


::::: {.container}
:::: {.about .px-md-7 .py-3 style="border-bottom: 1px solid #C8C7C7;"} 
# While the core mission science focuses on observations of atmospheric composition, the mission will also observe surface properties at unmatched spectral resolutions.  

Carbon-I mission is designed as a wide-swath mapping mission, collecting shortwave infrared imaging spectroscopy data. This will provide new insights into a wide range of areas including critical mineral distributions, vegetation stress, and pollution. Carbon-I’s higher spectral resolution in the shortwave infrared compared to existing missions (e.g., EMIT) allows clearer discrimination of minerals such as lithium-bearing hectorite from similar materials, offering significant value for scientific analysis and potential resource exploration. The Carbon-I team has taken surface reflectance measurements with the Carbon-I Demonstration Unit in the laboratory - check them out below with this tool which allows you to compare these highly resolved spectral data with coarser spectral resolutions to see the power of Carbon-I's surface spectra.

:::: 
:::: {.plots .px-md-4 .py-3}
```{ojs}
import { rangeSlider as rangeSlider } from '@mootari/range-slider'
```

```{ojs}
rawData =  FileAttachment("data/rfl_lowpass_shaped_v2.csv").csv({typed: true});
```

::: {.grid .py-1 }
::: {.g-col-6}
```{ojs}
fmt = d3.format(".2f");

viewof slider = rangeSlider({
  min: d3.extent(rawData, d => d.wavelength)[0],
  max: d3.extent(rawData, d => d.wavelength)[1],
  step: 0.7,
  format: d => `${fmt(d)} nm`,
  title: 'Select a range',
})

minWL = slider[0];
maxWL = slider[1];
```
:::
::: {.g-col-4}
```{ojs}
viewof normalize = Inputs.toggle({
  label: "Normalize 0–1 (per sample)",
  value: false
})
```
:::

```{ojs}
categoryLookup = new Map([
  ["Hectorite", {category: "Minerals", color: "#4daf4a"}],  
  ["Magnesite", {category: "Minerals", color: "#ff7f00"}], 
  ["Saponite", {category: "Minerals", color: "#a65628"}], 
  ["Sepiolite", {category: "Minerals", color: "#e377c2"}], 
  ["Vermiculite", {category: "Minerals", color: "#7f7f7f"}],  

  ["Oak leaf, new growth", {category: "Vegetation", color: "#3a8040"}],  
  ["Oak leaf, one year old", {category: "Vegetation", color: "#ffdd57"}],
  ["Oak leaf, scenesced", {category: "Vegetation", color: "#1b9e77"}],
  ["Maple Leaf", {category: "Vegetation", color: "#7570b3"}],

  ["HDPE bottle",  {category: "Plastics", color: "#1f78b4"}], 
  ["HDPE film", {category: "Plastics", color: "#33a02c"}], 
  ["PVC", {category: "Plastics", color: "#6a3d9a"}],
  ["Polysterene", {category: "Plastics", color: "#b15928"}]   
]);
```

```{ojs}
samples = d3.sort(d3.group(rawData, d => d.sample).keys());

categorizedData = rawData.map(d => {
  const info = categoryLookup.get(d.sample)
  return {
    ...d,
    category : info.category,  
    color : info.color               
  };
});

// wrapping data in set removes duplicates
categories = Array.from(new Set(categorizedData.map(d => d.category)));
```

::: {.g-col-6}
```{ojs}
viewof categorySel = Inputs.checkbox(categories, {
  label: "Categories", 
  value: ["Plastics"]}
);
sampleOptions = Array.from(categoryLookup.keys()).filter(
  s => samples.includes(s) && categorySel.includes(categoryLookup.get(s).category)
);
```
:::
::: {.g-col-6}
```{ojs}
viewof sampleSel = Inputs.checkbox(sampleOptions, {
  value: sampleOptions 
});
viewof samplePanel = categorySel.length
  ? html`<details class="sample-drop" open>
           <summary style="color:#000;">Samples</summary>
           ${viewof sampleSel}
         </details>`
  : html``;
```
:::
::: {.g-col-6}
```{ojs}
viewof resolutionSel = Inputs.range([0.7, 15.4], {step: 0.7, value: 0.7, label: "Resolution (nm)"})
```
:::
::: {.g-col-6}
```{ojs}
viewof windowSel = Inputs.range([1, 15], {step: 1, value: 1, label: "Spectral Averaging Window"})
```
:::
:::
```{ojs}
filteredData = categorizedData.filter(
  d => d.wavelength >= minWL && 
  d.wavelength <= maxWL && 
  categorySel.includes(d.category) &&
  sampleSel.includes(d.sample)
);
```

```{ojs}
{
  if (filteredData.length < 1) {
    return html`<div style="color:#666;">No active selections to plot.</div>`;
  }

  const rowsBySample = d3.group(filteredData, d => d.sample);  // Map<sample, rows[]>
  const adjustedData = [];
  const resolutionSize = Math.max(1, Math.round(resolutionSel / 0.7)); 

  for (const [sample, rows] of rowsBySample) {
    let adjusted = rows.map(r => r.reflectance);

    if (windowSel > 1){
      adjusted = d3.blur(adjusted, windowSel);
    } 

    let ymin, ymax, denom;
    if (normalize) {
      ymin = d3.min(adjusted);
      ymax = d3.max(adjusted);
      denom = ymax - ymin; 
    }

    for (let i = 0; i < adjusted.length; i += resolutionSize) {
      let y = adjusted[i];
      if (normalize){
        y = denom > 0 ? (adjusted[i] - ymin) / denom : 0 //avoid divide by 0
      }
      
      adjustedData.push({
        wavelength: rows[i].wavelength,
        reflectance: y,
        sample
      });
    }
  }

  const activeSamples = Array.from(new Set(filteredData.map(d => d.sample)));
  const plotWidth = Math.min(width, 1500);
  const fontPx = Math.max(10, Math.round(plotWidth / 70));
  const rotate = plotWidth < 650 ? -45 : 0;

  const tickStart = Math.ceil(minWL / 30) * 30;
  const tickEnd = Math.floor(maxWL / 30) * 30;

  const yLabel = normalize ? "Normalized reflectance" : "Reflectance";
  const maxY = normalize ? 1.03 : d3.max(adjustedData, d => d.reflectance) * 1.03;

  const fmtVal = normalize ? d3.format(".3f") : d3.format(".4f");

  return Plot.plot({
    width: plotWidth,
    height: Math.round(plotWidth * (13/20)),
    marginRight: 150,
    marginLeft: 55,   
    marginTop: 40,   
    marginBottom: 50,

    x: {
      label: "Wavelength (nm)",
      grid: true,
      tickPadding: 10,
      tickRotate: rotate,
      ticks: d3.range(tickStart, tickEnd + 1, 30),
      tickFormat: d => d.toFixed(2),
    },
    y: {
      label: yLabel,
      grid: true,
      tickPadding: 10,
      domain: [0, maxY]
    },
    color: {
      legend: true,
      domain: activeSamples,
      range: activeSamples.map(s => categoryLookup.get(s).color),
      inset: { 
        anchor: "top-right", 
        inset: 10, 
        padding: 5 
      }
    },
    style: { 
      fontSize: `${fontPx}px`, 
      "--plot-font-size": `${fontPx}px` 
    },

    marks: [
      Plot.line(adjustedData, {
        x: "wavelength",
        y: "reflectance",
        stroke: "sample",
        title: "sample",
        strokeWidth: 4
      }),
      Plot.dot(adjustedData, Plot.pointerX({
        x: "wavelength",
        y: "reflectance",
        fill: "sample",
        stroke: "white",
        marker: "circle",
        strokeWidth: 2.5,
        r: 5
      })),
      Plot.text(adjustedData, Plot.pointerX({
        px: "wavelength",
        py: "reflectance",
        frameAnchor: "top-left",
        fontVariant: "tabular-nums",
        fontSize: fontPx * 0.8,
        text: d => `${d.sample}   λ ${fmt(d.wavelength)} nm    ${fmtVal(d.reflectance)}`
      })),
      Plot.text(adjustedData, Plot.selectLast({
        x: "wavelength",
        y: "reflectance",
        z: "sample",
        text: "sample",
        textAnchor: "start",
        fontSize: fontPx * 0.7,
        dx: 3
      }))
    ]
  });
}

```
::::
:::::


Lead Institution


Instrument and Mission Management


Spacecraft and Mission Operations