| import React, { createContext, useContext, useState, useMemo, useCallback } from 'react' |
|
|
| const XRDContext = createContext() |
|
|
| export const useXRD = () => { |
| const context = useContext(XRDContext) |
| if (!context) { |
| throw new Error('useXRD must be used within XRDProvider') |
| } |
| return context |
| } |
|
|
| export const XRDProvider = ({ children }) => { |
| |
| const MODEL_INPUT_SIZE = 8192 |
| const MODEL_WAVELENGTH = 0.6199 |
| const MODEL_MIN_2THETA = 5.0 |
| const MODEL_MAX_2THETA = 20.0 |
| |
| |
| const [rawData, setRawData] = useState(null) |
| const [filename, setFilename] = useState(null) |
| |
| |
| const [detectedWavelength, setDetectedWavelength] = useState(null) |
| const [userWavelength, setUserWavelength] = useState(MODEL_WAVELENGTH) |
| const [wavelengthSource, setWavelengthSource] = useState('default') |
| |
| |
| const [baselineCorrection, setBaselineCorrection] = useState(false) |
| const [interpolationEnabled, setInterpolationEnabled] = useState(true) |
| const [scalingEnabled, setScalingEnabled] = useState(true) |
| const [interpolationStrategy, setInterpolationStrategy] = useState('linear') |
| |
| |
| const [dataWarnings, setDataWarnings] = useState([]) |
| |
| |
| const [modelResults, setModelResults] = useState(null) |
| const [isLoading, setIsLoading] = useState(false) |
| const [analysisStatus, setAnalysisStatus] = useState('IDLE') |
| |
| |
| const [isLogitDrawerOpen, setIsLogitDrawerOpen] = useState(false) |
| |
| |
| const [analysisCount, setAnalysisCount] = useState(0) |
| |
| |
| |
| const 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 |
| } |
| |
| |
| const 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 } |
| } |
| |
| |
| const processedData = useMemo(() => { |
| if (!rawData) return null |
| |
| try { |
| const warnings = [] |
| let processedY = [...rawData.y] |
| let processedX = [...rawData.x] |
| |
| |
| const sourceWavelength = userWavelength |
| if (sourceWavelength && Math.abs(sourceWavelength - MODEL_WAVELENGTH) > 0.0001) { |
| const convertedData = [] |
| for (let i = 0; i < processedX.length; i++) { |
| const convertedTheta = convertWavelength(processedX[i], sourceWavelength, MODEL_WAVELENGTH) |
| if (convertedTheta !== null) { |
| convertedData.push({ x: convertedTheta, y: processedY[i] }) |
| } |
| } |
| |
| if (convertedData.length < processedX.length) { |
| warnings.push(`${processedX.length - convertedData.length} points outside physical range after wavelength conversion`) |
| } |
| |
| processedX = convertedData.map(d => d.x) |
| processedY = convertedData.map(d => d.y) |
| |
| warnings.push(`Converted from ${sourceWavelength.toFixed(4)} Å to ${MODEL_WAVELENGTH} Å`) |
| } |
| |
| |
| if (baselineCorrection) { |
| const baseline = Math.min(...processedY) |
| processedY = processedY.map(val => val - baseline) |
| } |
| |
| |
| const inRangeData = [] |
| for (let i = 0; i < processedX.length; i++) { |
| if (processedX[i] >= MODEL_MIN_2THETA && processedX[i] <= MODEL_MAX_2THETA) { |
| inRangeData.push({ x: processedX[i], y: processedY[i] }) |
| } |
| } |
| |
| if (inRangeData.length === 0) { |
| warnings.push(`⚠️ No data points in model range (${MODEL_MIN_2THETA}-${MODEL_MAX_2THETA}°)`) |
| |
| inRangeData.push(...processedX.map((x, i) => ({ x, y: processedY[i] }))) |
| } else if (inRangeData.length < processedX.length) { |
| const coverage = (inRangeData.length / processedX.length * 100).toFixed(1) |
| warnings.push(`${coverage}% of data in model range (${MODEL_MIN_2THETA}-${MODEL_MAX_2THETA}°)`) |
| } |
| |
| let croppedX = inRangeData.map(d => d.x) |
| let croppedY = inRangeData.map(d => d.y) |
| |
| |
| |
| if (scalingEnabled) { |
| const minY = Math.min(...croppedY) |
| const maxY = Math.max(...croppedY) |
| if (maxY - minY > 0) { |
| croppedY = croppedY.map(val => ((val - minY) / (maxY - minY)) * 100) |
| } |
| } |
| |
| |
| const interpolated = interpolateData( |
| croppedX, |
| croppedY, |
| MODEL_INPUT_SIZE, |
| MODEL_MIN_2THETA, |
| MODEL_MAX_2THETA, |
| interpolationStrategy |
| ) |
| |
| |
| setDataWarnings(warnings) |
| |
| return { |
| x: interpolated.x, |
| y: interpolated.y |
| } |
| } catch (error) { |
| console.error('Error processing data:', error) |
| setDataWarnings([`Error: ${error.message}`]) |
| return rawData |
| } |
| }, [rawData, baselineCorrection, userWavelength, interpolationStrategy, scalingEnabled]) |
| |
| |
| const 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 |
| } |
| |
| |
| const parseCIF = (text) => { |
| const lines = text.split('\n') |
| const x = [] |
| const y = [] |
| let inDataLoop = false |
| let dataColumns = [] |
| let thetaIndex = -1 |
| let intensityIndex = -1 |
| |
| for (let i = 0; i < lines.length; i++) { |
| const line = lines[i].trim() |
| |
| |
| if (line === 'loop_') { |
| inDataLoop = true |
| dataColumns = [] |
| continue |
| } |
| |
| |
| if (inDataLoop && line.startsWith('_')) { |
| dataColumns.push(line) |
| |
| |
| if (/_pd_meas_angle_2theta/i.test(line) || /_pd_calc_angle_2theta/i.test(line)) { |
| thetaIndex = dataColumns.length - 1 |
| } |
| |
| |
| if (/_pd_proc_intensity/i.test(line) || /_pd_calc_intensity/i.test(line) || /_pd_meas_counts/i.test(line)) { |
| intensityIndex = dataColumns.length - 1 |
| } |
| continue |
| } |
| |
| |
| if (inDataLoop && !line.startsWith('_') && !line.startsWith('loop_') && line.length > 0 && !line.startsWith('#')) { |
| |
| if (thetaIndex >= 0 && intensityIndex >= 0) { |
| const parts = line.split(/\s+/) |
| |
| if (parts.length >= Math.max(thetaIndex, intensityIndex) + 1) { |
| const xVal = parseFloat(parts[thetaIndex]) |
| const yVal = parseFloat(parts[intensityIndex]) |
| |
| if (!isNaN(xVal) && !isNaN(yVal)) { |
| x.push(xVal) |
| y.push(yVal) |
| } |
| } |
| } else { |
| |
| inDataLoop = false |
| dataColumns = [] |
| thetaIndex = -1 |
| intensityIndex = -1 |
| } |
| } |
| |
| |
| if (inDataLoop && (line.startsWith('data_') || (line === 'loop_' && dataColumns.length > 0))) { |
| inDataLoop = false |
| } |
| } |
| |
| return { x, y } |
| } |
| |
| |
| const 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 } |
| } |
| |
| |
| const parseFile = (file) => { |
| return new Promise((resolve, reject) => { |
| const reader = new FileReader() |
| |
| reader.onload = (e) => { |
| try { |
| const text = e.target.result |
| |
| |
| const metadata = extractMetadata(text) |
| if (metadata.wavelength) { |
| setDetectedWavelength(metadata.wavelength) |
| setUserWavelength(metadata.wavelength) |
| setWavelengthSource('detected') |
| } else { |
| setDetectedWavelength(null) |
| setWavelengthSource('default') |
| } |
| |
| |
| const fileName = file.name.toLowerCase() |
| let data = { x: [], y: [] } |
| |
| if (fileName.endsWith('.cif')) { |
| |
| data = parseCIF(text) |
| |
| |
| if (data.x.length === 0) { |
| console.log('CIF loop parsing failed, falling back to simple parser') |
| data = parseDIF(text) |
| } |
| } else { |
| |
| data = parseDIF(text) |
| } |
| |
| if (data.x.length === 0 || data.y.length === 0) { |
| reject(new Error('No valid data points found in file')) |
| return |
| } |
| |
| console.log(`Parsed ${data.x.length} data points from ${fileName}`) |
| resolve(data) |
| } catch (error) { |
| reject(error) |
| } |
| } |
| |
| reader.onerror = () => reject(new Error('Failed to read file')) |
| reader.readAsText(file) |
| }) |
| } |
| |
| |
| const handleFileUpload = async (file) => { |
| try { |
| const data = await parseFile(file) |
| |
| setRawData(data) |
| setFilename(file.name) |
| setModelResults(null) |
| setAnalysisStatus('IDLE') |
| setIsLogitDrawerOpen(false) |
| |
| return true |
| } catch (error) { |
| console.error('Error uploading file:', error) |
| alert(`Error loading file: ${error.message}`) |
| return false |
| } |
| } |
| |
| |
| const runInference = useCallback(async () => { |
| if (!processedData) { |
| alert('No data to analyze') |
| return |
| } |
| |
| |
| const currentCount = analysisCount + 1 |
| setAnalysisCount(currentCount) |
| |
| setIsLoading(true) |
| setAnalysisStatus('PROCESSING') |
| |
| try { |
| const requestTimestamp = Date.now() |
| |
| const response = await fetch('/api/predict', { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json', |
| |
| 'Cache-Control': 'no-cache, no-store, must-revalidate', |
| 'Pragma': 'no-cache', |
| 'Expires': '0', |
| |
| 'X-Request-ID': String(requestTimestamp), |
| 'X-Filename': filename || 'unknown', |
| }, |
| |
| cache: 'no-store', |
| body: JSON.stringify({ |
| x: processedData.x, |
| y: processedData.y, |
| |
| metadata: { |
| timestamp: requestTimestamp, |
| filename: filename, |
| analysisCount: currentCount, |
| } |
| }), |
| }) |
| |
| if (!response.ok) { |
| throw new Error(`API error: ${response.status}`) |
| } |
| |
| const results = await response.json() |
| setModelResults(results) |
| setAnalysisStatus('COMPLETE') |
| } catch (error) { |
| console.error('Error running inference:', error) |
| alert(`Inference failed: ${error.message}`) |
| setAnalysisStatus('IDLE') |
| } finally { |
| setIsLoading(false) |
| } |
| }, [processedData, analysisCount, filename]) |
| |
| |
| const loadExampleFile = useCallback(async (filename) => { |
| try { |
| const response = await fetch(`/api/examples/${encodeURIComponent(filename)}`) |
| if (!response.ok) { |
| throw new Error(`Failed to fetch example: ${response.status}`) |
| } |
| const text = await response.text() |
|
|
| |
| const metadata = extractMetadata(text) |
| if (metadata.wavelength) { |
| setDetectedWavelength(metadata.wavelength) |
| setUserWavelength(metadata.wavelength) |
| setWavelengthSource('detected') |
| } else { |
| setDetectedWavelength(null) |
| setWavelengthSource('default') |
| } |
|
|
| |
| const data = parseDIF(text) |
| if (data.x.length === 0 || data.y.length === 0) { |
| throw new Error('No valid data points found in example file') |
| } |
|
|
| setRawData(data) |
| setFilename(filename) |
| setModelResults(null) |
| setAnalysisStatus('IDLE') |
| setIsLogitDrawerOpen(false) |
|
|
| return true |
| } catch (error) { |
| console.error('Error loading example file:', error) |
| alert(`Error loading example: ${error.message}`) |
| return false |
| } |
| }, []) |
|
|
| |
| const handleReset = () => { |
| setRawData(null) |
| setFilename(null) |
| setModelResults(null) |
| setAnalysisStatus('IDLE') |
| setIsLogitDrawerOpen(false) |
| } |
| |
| const value = { |
| rawData, |
| processedData, |
| modelResults, |
| isLoading, |
| filename, |
| analysisStatus, |
| detectedWavelength, |
| userWavelength, |
| setUserWavelength, |
| wavelengthSource, |
| dataWarnings, |
| baselineCorrection, |
| setBaselineCorrection, |
| interpolationEnabled, |
| setInterpolationEnabled, |
| scalingEnabled, |
| setScalingEnabled, |
| interpolationStrategy, |
| setInterpolationStrategy, |
| isLogitDrawerOpen, |
| setIsLogitDrawerOpen, |
| handleFileUpload, |
| loadExampleFile, |
| runInference, |
| handleReset, |
| MODEL_WAVELENGTH, |
| MODEL_MIN_2THETA, |
| MODEL_MAX_2THETA, |
| MODEL_INPUT_SIZE, |
| } |
| |
| return <XRDContext.Provider value={value}>{children}</XRDContext.Provider> |
| } |
|
|