OpenAlphaDiffract-UI / frontend /src /utils /xrd-processing.js
linked-liszt's picture
Upload folder using huggingface_hub
6d08d46 verified
/**
* Pure utility functions extracted from XRDContext for testability.
* These functions contain no React state or side effects.
*/
/**
* Convert wavelength using Bragg's law: lambda = 2d*sin(theta)
* For same d-spacing: sin(theta2) = (lambda2/lambda1) * sin(theta1)
*
* @param {number} theta_deg - 2theta angle in degrees
* @param {number} sourceWavelength - Source wavelength in Angstroms
* @param {number} targetWavelength - Target wavelength in Angstroms
* @returns {number|null} Converted 2theta angle, or null if physically impossible
*/
export function convertWavelength(theta_deg, sourceWavelength, targetWavelength) {
if (Math.abs(sourceWavelength - targetWavelength) < 0.0001) {
return theta_deg
}
const theta_rad = (theta_deg * Math.PI) / 180
const sin_theta2 = (targetWavelength / sourceWavelength) * Math.sin(theta_rad)
if (Math.abs(sin_theta2) > 1) {
return null
}
const theta2_rad = Math.asin(sin_theta2)
return (theta2_rad * 180) / Math.PI
}
/**
* Interpolate data to fixed size for model input.
*
* @param {number[]} x - Input x values (2theta)
* @param {number[]} y - Input y values (intensity)
* @param {number} targetSize - Desired output length
* @param {number} [xMin] - Minimum x value for output grid
* @param {number} [xMax] - Maximum x value for output grid
* @param {string} [strategy='linear'] - Interpolation strategy: 'linear' or 'cubic'
* @returns {{x: number[], y: number[]}} Interpolated data
*/
export function interpolateData(x, y, targetSize, xMin, xMax, strategy = 'linear') {
if (x.length === targetSize && xMin === undefined) {
return { x, y }
}
const minX = xMin !== undefined ? xMin : Math.min(...x)
const maxX = xMax !== undefined ? xMax : Math.max(...x)
const step = (maxX - minX) / (targetSize - 1)
const newX = Array.from({ length: targetSize }, (_, i) => minX + i * step)
const newY = new Array(targetSize)
const dataMinX = Math.min(...x)
const dataMaxX = Math.max(...x)
if (strategy === 'linear') {
for (let i = 0; i < targetSize; i++) {
const targetX = newX[i]
if (targetX < dataMinX || targetX > dataMaxX) {
newY[i] = 0
continue
}
let idx = x.findIndex(val => val >= targetX)
if (idx === -1) idx = x.length - 1
if (idx === 0) idx = 1
const x0 = x[idx - 1]
const x1 = x[idx]
const y0 = y[idx - 1]
const y1 = y[idx]
newY[i] = y0 + ((targetX - x0) * (y1 - y0)) / (x1 - x0)
}
} else if (strategy === 'cubic') {
for (let i = 0; i < targetSize; i++) {
const targetX = newX[i]
if (targetX < dataMinX || targetX > dataMaxX) {
newY[i] = 0
continue
}
let idx = x.findIndex(val => val >= targetX)
if (idx === -1) idx = x.length - 1
if (idx === 0) idx = 1
const i0 = Math.max(0, idx - 2)
const i1 = Math.max(0, idx - 1)
const i2 = Math.min(x.length - 1, idx)
const i3 = Math.min(x.length - 1, idx + 1)
if (i2 === i1) {
newY[i] = y[i1]
} else {
const t = (targetX - x[i1]) / (x[i2] - x[i1])
const t2 = t * t
const t3 = t2 * t
const v0 = y[i0]
const v1 = y[i1]
const v2 = y[i2]
const v3 = y[i3]
newY[i] = 0.5 * (
2 * v1 +
(-v0 + v2) * t +
(2 * v0 - 5 * v1 + 4 * v2 - v3) * t2 +
(-v0 + 3 * v1 - 3 * v2 + v3) * t3
)
}
}
}
return { x: newX, y: newY }
}
/**
* Parse DIF or XY format (space-separated 2theta intensity).
*
* @param {string} text - Raw file content
* @returns {{x: number[], y: number[]}} Parsed data points
*/
export function parseDIF(text) {
const lines = text.split('\n')
const x = []
const y = []
for (const line of lines) {
const trimmed = line.trim()
if (!trimmed ||
trimmed.startsWith('#') ||
trimmed.startsWith('_') ||
trimmed.startsWith('CELL') ||
trimmed.startsWith('SPACE') ||
/^[a-zA-Z]/.test(trimmed)) {
continue
}
const parts = trimmed.split(/\s+/)
if (parts.length >= 2) {
const xVal = parseFloat(parts[0])
const yVal = parseFloat(parts[1])
if (!isNaN(xVal) && !isNaN(yVal)) {
x.push(xVal)
y.push(yVal)
}
}
}
return { x, y }
}
/**
* Extract metadata from CIF/DIF file text.
*
* @param {string} text - Raw file content
* @returns {{wavelength: number|null, cellParams: string|null, spaceGroup: string|null, crystalSystem: string|null}}
*/
export function extractMetadata(text) {
const metadata = {
wavelength: null,
cellParams: null,
spaceGroup: null,
crystalSystem: null
}
const lines = text.split('\n')
const wavelengthPatterns = [
/wavelength[:\s=]+([0-9.]+)/i,
/lambda[:\s=]+([0-9.]+)/i,
/wave[:\s=]+([0-9.]+)/i,
/_pd_wavelength[:\s]+([0-9.]+)/i,
/_diffrn_radiation_wavelength[:\s]+([0-9.]+)/i,
/radiation.*?([0-9.]+)\s*[AÅ]/i,
]
for (const line of lines) {
if (!metadata.wavelength) {
for (const pattern of wavelengthPatterns) {
const match = line.match(pattern)
if (match && match[1]) {
const wavelength = parseFloat(match[1])
if (wavelength > 0.1 && wavelength < 3.0) {
metadata.wavelength = wavelength
break
}
}
}
if (/Cu\s*K[αa]/i.test(line)) metadata.wavelength = 1.5406
else if (/Mo\s*K[αa]/i.test(line)) metadata.wavelength = 0.7107
else if (/Co\s*K[αa]/i.test(line)) metadata.wavelength = 1.7889
else if (/Cr\s*K[αa]/i.test(line)) metadata.wavelength = 2.2897
}
if (/CELL PARAMETERS:/i.test(line)) {
const match = line.match(/CELL PARAMETERS:\s*([\d.\s]+)/)
if (match) {
metadata.cellParams = match[1].trim()
}
}
if (/SPACE GROUP:/i.test(line) || /_symmetry_Int_Tables_number/i.test(line)) {
const match = line.match(/(?:SPACE GROUP:|_symmetry_Int_Tables_number)[:\s]+(\d+)/)
if (match) {
metadata.spaceGroup = match[1]
}
}
if (/Crystal System:/i.test(line)) {
const match = line.match(/Crystal System:\s*(\d+)/)
if (match) {
metadata.crystalSystem = match[1]
}
}
}
return metadata
}