Skip to content

Commit

Permalink
Merge 2dcf371 into 21ed72d
Browse files Browse the repository at this point in the history
  • Loading branch information
ml-evs authored Oct 16, 2024
2 parents 21ed72d + 2dcf371 commit 4cad5ea
Show file tree
Hide file tree
Showing 7 changed files with 307 additions and 1 deletion.
3 changes: 3 additions & 0 deletions pydatalab/src/pydatalab/apps/raman_map/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .blocks import RamanMapBlock

__all__ = ("RamanMapBlock",)
217 changes: 217 additions & 0 deletions pydatalab/src/pydatalab/apps/raman_map/blocks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import os
import warnings
from pathlib import Path

import bokeh
import numpy as np
import PIL
from bokeh.layouts import column
from bokeh.models import ColorBar, ColumnDataSource, LinearColorMapper
from pybaselines import Baseline
from rsciio.renishaw import file_reader

from pydatalab.blocks.base import DataBlock
from pydatalab.file_utils import get_file_info_by_id


class RamanMapBlock(DataBlock):
blocktype = "raman_map"
name = "Raman spectroscopy map"
description = "Visualize 2D maps of Raman spectroscopy data."
accepted_file_extensions = (".wdf",)

@property
def plot_functions(self):
return (self.generate_raman_map_plot,)

@classmethod
def load(cls, location: Path | str) -> tuple[np.ndarray, np.ndarray, dict]:
"""Read the .wdf file with RosettaSciIO and extract the image
and points of the Raman measurements. Plots these as an image
overlaid by a scatter plot with points of gradient colours
Parameters:
location: The location of the file to read.
Returns:
raman_shift: list of numbers corresponding to colors of each scatter point
map: The raw Raman data, containing the map as a (x, y, spectra_len) array.
metadata: metadata associated witht the measurement
"""

raman_data = file_reader(location)

if len(raman_data[0]["axes"]) != 3:
raise RuntimeError("Data is not compatible with 1D or 2D Raman data.")

for d in raman_data[0]["axes"]:
if d["name"] == "Raman Shift":
raman_shift = []
for i in range(int(d["size"])):
raman_shift.append(float(d["offset"]) + float(d["scale"]) * i)

return np.array(raman_shift), raman_data[0], raman_data[0]["metadata"]

@classmethod
def plot_raman_map(cls, location: str | Path):
"""Read the .wdf file with RosettaSciIO and extract relevant
data.
Parameters:
location: The location of the file to read.
Returns:
col: list of numbers corresponding to colors of each scatter
point
p: plot of image and points Raman was measured
metadata: metadata associated witht the measurement
"""
raman_shifts, data, metadata = cls.load(location)
x_coordinates = []
# gets the size, point spacing and original offset of x-axis
size_x = data["original_metadata"]["WMAP_0"]["size_xyz"][0]
scale_x = data["original_metadata"]["WMAP_0"]["scale_xyz"][0]
offset_x = data["original_metadata"]["WMAP_0"]["offset_xyz"][0]
# generates x-coordinates
for i in range(size_x):
x_coordinates.append(i * scale_x + offset_x)
y_coordinates = []
# gets the size, point spacing and original offset of x-axis
size_y = data["original_metadata"]["WMAP_0"]["size_xyz"][1]
scale_y = data["original_metadata"]["WMAP_0"]["scale_xyz"][1]
offset_y = data["original_metadata"]["WMAP_0"]["offset_xyz"][1]
# generates y-coordinates
for i in range(size_y):
y_coordinates.append(i * scale_y + offset_y)

coordinate_pairs = []
for y in y_coordinates:
for x in x_coordinates:
coordinate_pairs.append((x, y))

# extracts image and gets relevant data
image_data = data["original_metadata"]["WHTL_0"]["image"]
image = PIL.Image.open(image_data)
origin = data["original_metadata"]["WHTL_0"]["FocalPlaneXYOrigins"]
origin = [float(origin[0]), float(origin[1])]
x_span = float(data["original_metadata"]["WHTL_0"]["FocalPlaneXResolution"])
y_span = float(data["original_metadata"]["WHTL_0"]["FocalPlaneYResolution"])
# converts image to vector compatible with bokeh
image_array = np.array(image, dtype=np.uint8)
image_array = np.flip(image_array, axis=0)
image_array = np.dstack((image_array, 255 * np.ones_like(image_array[:, :, 0])))
img_vector = image_array.view(dtype=np.uint32).reshape(
(image_array.shape[0], image_array.shape[1])
)
# generates numbers for colours for points in linear gradient
col = [
i / (len(x_coordinates) * len(y_coordinates))
for i in range(len(x_coordinates) * len(y_coordinates))
]
# links x- and y-coordinates with colour numbers
source = ColumnDataSource(
data={
"x": [pair[0] for pair in coordinate_pairs],
"y": [pair[1] for pair in coordinate_pairs],
"col": col,
}
)
# gemerates colormap for coloured scatter poitns
exp_cmap = LinearColorMapper(palette="Turbo256", low=min(col), high=max(col))
# generates image figure and plots image
# p.image_rgba(image=[img_vector], x=origin[0], y=origin[1], dw=x_span, dh=y_span)
p = bokeh.plotting.figure(
width=image_array.shape[1],
height=image_array.shape[0],
x_range=(origin[0], origin[0] + x_span),
y_range=(origin[1] + y_span, origin[1]),
)
p.image_rgba(image=[img_vector], x=origin[0], y=origin[1] + y_span, dw=x_span, dh=y_span)
# plot scatter points and colorbar
p.circle("x", "y", size=10, source=source, color={"field": "col", "transform": exp_cmap})
color_bar = ColorBar(
color_mapper=exp_cmap, label_standoff=12, border_line_color=None, location=(0, 0)
)
p.add_layout(color_bar, "right")
bokeh.plotting.save(p)
# return color numbers to use in spectra plotting and returns figure object

bokeh.plotting.output_file(Path(__file__).parent / "plot.html")
return col, p, metadata

@classmethod
def plot_raman_spectra(cls, location: str | Path, col):
"""Read the .wdf file with RosettaSciIO and extract relevant
data.
Parameters:
location: The location of the file to read.
col: list of numbers corresponding to colors of the points generated
in the map plot
Returns:
Bokeh plot of the Raman spectra
"""
# generates plot and extracts raman spectra from .wdf file
p = bokeh.plotting.figure(
width=800,
height=400,
x_axis_label="Raman Shift (cm-1)",
y_axis_label="Intensity (a.u.)",
)
raman_shift, data, metadata = cls.load(location)
intensity_list = []
intensity_data = data["data"]

# generates baseline to be subtracted from spectra
def generate_baseline(x_data, y_data):
baseline_fitter = Baseline(x_data=x_data)
baseline = baseline_fitter.mor(y_data, half_window=30)[0]
return baseline

# the bokeh ColumnDataSource works was easiest for me to work with this as a list so making list of spectra intensities
for i in range(intensity_data.shape[0]):
for j in range(intensity_data.shape[1]):
intensity_spectrum = intensity_data[i, j, :]
# want to make optional but will leave for now
baseline = generate_baseline(raman_shift, intensity_spectrum)
intensity_spectrum = intensity_spectrum - baseline
intensity_list.append(intensity_spectrum)

# generates colorbar
source = ColumnDataSource(
data={"x": [raman_shift] * len(intensity_list), "y": intensity_list, "col": col}
)
exp_cmap = LinearColorMapper(palette="Turbo256", low=min(col), high=max(col))
# plots spectra
p.multi_line(
"x", "y", line_width=0.5, source=source, color={"field": "col", "transform": exp_cmap}
)
return p

def generate_raman_map_plot(self):
file_info = None

warnings.warn(
"The alignment of spectra grid and image cannot yet be guaranteed; please verify any plot you create."
)

if "file_id" not in self.data:
return None

else:
file_info = get_file_info_by_id(self.data["file_id"], update_if_live=True)
ext = os.path.splitext(file_info["location"].split("/")[-1])[-1].lower()
if ext not in self.accepted_file_extensions:
raise RuntimeError(
"RamanBlock.generate_raman_plot(): Unsupported file extension (must be one of %s), not %s",
self.accepted_file_extensions,
ext,
)
col, p1, metadata = self.plot_raman_map(file_info["location"])
p2 = self.plot_raman_spectra(file_info["location"], col=col)
self.data["bokeh_plot_data"] = bokeh.embed.json_item(column(p1, p2))
3 changes: 3 additions & 0 deletions pydatalab/src/pydatalab/blocks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from pydatalab.apps.eis import EISBlock
from pydatalab.apps.nmr import NMRBlock
from pydatalab.apps.raman import RamanBlock
from pydatalab.apps.raman_map import RamanMapBlock
from pydatalab.apps.tga import MassSpecBlock
from pydatalab.apps.xrd import XRDBlock
from pydatalab.blocks.base import DataBlock
Expand All @@ -17,6 +18,7 @@
XRDBlock,
CycleBlock,
RamanBlock,
RamanMapBlock,
NMRBlock,
NotSupportedBlock,
MassSpecBlock,
Expand All @@ -37,6 +39,7 @@
"NotSupportedBlock",
"NMRBlock",
"RamanBlock",
"RamanMapBlock",
"MassSpecBlock",
"TabularDataBlock",
"BLOCK_TYPES",
Expand Down
2 changes: 1 addition & 1 deletion pydatalab/src/pydatalab/blocks/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def load(cls, location: Path) -> pd.DataFrame:

if df.isnull().values.any():
warnings.warn(
"Loading file with less strict parser: columns were previously detected as {df.columns}"
f"Loading file with less strict parser: columns were previously detected as {df.columns}"
)
df = pd.read_csv(
location,
Expand Down
23 changes: 23 additions & 0 deletions pydatalab/tests/apps/test_raman_map.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from pathlib import Path

import pytest

from pydatalab.apps.raman_map.blocks import RamanMapBlock


@pytest.fixture
def data_files():
return (Path(__file__).parent.parent.parent / "example_data" / "raman").glob("*map.wdf")


def test_load(data_files):
for f in data_files:
spectra, data, metadata = RamanMapBlock.load(f)
map = data["data"]
assert spectra.shape == (1011,)
assert map.shape == (5, 7, 1011)
assert "General" in metadata
assert "Signal" in metadata
assert "Acquisition_instrument" in metadata

RamanMapBlock.plot_raman_map(f)
58 changes: 58 additions & 0 deletions webapp/src/components/datablocks/RamanMapBlock.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<template>
<!-- think about elegant two-way binding to DataBlockBase... or, just pass all the block data into
DataBlockBase as a prop, and save from within DataBlockBase -->
<DataBlockBase :item_id="item_id" :block_id="block_id">
<FileSelectDropdown
v-model="file_id"
:item_id="item_id"
:block_id="block_id"
:extensions="['.wdf']"
update-block-on-change
/>

<div class="row">
<div id="bokehPlotContainer" class="col-xl-9 col-lg-10 col-md-11 mx-auto">
<BokehPlot :bokeh-plot-data="bokehPlotData" />
</div>
</div>
</DataBlockBase>
</template>

<script>
import DataBlockBase from "@/components/datablocks/DataBlockBase";
import FileSelectDropdown from "@/components/FileSelectDropdown";
import BokehPlot from "@/components/BokehPlot";
import { createComputedSetterForBlockField } from "@/field_utils.js";
import { updateBlockFromServer } from "@/server_fetch_utils.js";
export default {
props: {
item_id: String,
block_id: String,
},
computed: {
bokehPlotData() {
return this.$store.state.all_item_data[this.item_id]["blocks_obj"][this.block_id]
.bokeh_plot_data;
},
file_id: createComputedSetterForBlockField("file_id"),
},
components: {
DataBlockBase,
FileSelectDropdown,
BokehPlot,
},
methods: {
updateBlock() {
updateBlockFromServer(
this.item_id,
this.block_id,
this.$store.state.all_item_data[this.item_id]["blocks_obj"][this.block_id],
);
},
},
};
</script>

<style scoped></style>
2 changes: 2 additions & 0 deletions webapp/src/resources.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import MediaBlock from "@/components/datablocks/MediaBlock";
import XRDBlock from "@/components/datablocks/XRDBlock";
import ChatBlock from "@/components/datablocks/ChatBlock";
import RamanBlock from "@/components/datablocks/RamanBlock";
import RamanMapBlock from "@/components/datablocks/RamanMapBlock";
import CycleBlock from "@/components/datablocks/CycleBlock";
import NMRBlock from "@/components/datablocks/NMRBlock";
import EISBlock from "@/components/datablocks/EISBlock";
Expand Down Expand Up @@ -56,6 +57,7 @@ export const blockTypes = {
tabular: { description: "Tabular Data", component: BokehBlock, name: "Tabular data" },
xrd: { description: "Powder XRD", component: XRDBlock, name: "Powder XRD" },
raman: { description: "Raman", component: RamanBlock, name: "Raman" },
raman_map: { description: "Raman Map", component: RamanMapBlock, name: "Raman 2D Map" },
cycle: { description: "Electrochemistry", component: CycleBlock, name: "Electrochemistry" },
eis: { description: "Electrochemical Impedance Spectroscopy", component: EISBlock, name: "EIS" },
nmr: { description: "Nuclear Magnetic Resonance Spectroscopy", component: NMRBlock, name: "NMR" },
Expand Down

0 comments on commit 4cad5ea

Please sign in to comment.