Spaces:
Sleeping
Sleeping
| /** | |
| * 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 | |
| } | |