diff --git a/.zenodo.json b/.zenodo.json index e40fe02..022712a 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -1,7 +1,7 @@ { "title": "Q-Shape - Quantitative Shape Analyzer", - "version": "v1.4.0", - "description": "Q-Shape: Advanced coordination geometry analysis using Continuous Shape Measures (CShM). A web-based tool for analyzing molecular coordination geometries with 92 reference geometries (CN 2-12, 20, 24, 48, 60), real-time 3D visualization, and comprehensive quality metrics.", + "version": "v1.5.0", + "description": "Q-Shape: Advanced coordination geometry analysis using Continuous Shape Measures (CShM). A web-based tool for analyzing molecular coordination geometries with 92 reference geometries (CN 2-12, 20, 24, 48, 60), real-time 3D visualization, and comprehensive quality metrics. Supports XYZ and CIF file formats with multi-structure analysis.", "creators": [ { "name": "Castro Silva Junior, Henrique", @@ -20,7 +20,10 @@ "computational chemistry", "web application", "Three.js", - "React" + "React", + "CIF", + "XYZ", + "crystallography" ], "license": { "id": "MIT" diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dc885c..668080f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,14 +5,41 @@ All notable changes to Q-Shape will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.5.0] - 2025-01-28 +## [1.5.0] - 2026-01-09 ### ๐ŸŽฏ Overview -**Piano Stool Complex Support** - This release adds comprehensive support for half-sandwich (piano stool) complexes, a major class of organometallic compounds that were previously not properly analyzed by the software. +**Multi-File Support & CIF Format** - This release adds support for multiple file formats, multi-geometry XYZ files, and simultaneous analysis of multiple structures. Additionally, comprehensive support for half-sandwich (piano stool) complexes has been added. ### โœจ New Features +#### Multi-Format File Support +- **CIF File Support** - Parse Crystallographic Information Files using the `crystcif-parse` library + - Automatic fractional-to-Cartesian coordinate conversion + - Multi-block CIF files supported (each data block becomes a separate structure) + - Cell parameters and space group metadata preserved + - File validation with informative error messages + +- **Multi-Geometry XYZ Files** - Read XYZ files containing multiple concatenated structures + - Automatic frame detection and counting + - Structure names extracted from comment lines + - Perfect for analyzing conformational isomers or trajectory snapshots + +- **Multiple File Upload** - Upload several XYZ/CIF files simultaneously + - All structures loaded and accessible via navigator + - Mix different file formats in a single session + +#### Structure Navigator UI +- **Dropdown Selector** - Easy switching between loaded structures +- **Navigation Buttons** - First/Prev/Next/Last for quick browsing (shown for >5 structures) +- **Structure Counter Badge** - Shows loaded structure count and file formats +- **Per-Structure Analysis** - Each structure analyzed independently with proper naming + +#### Enhanced Reports +- **Structure Names in PDF** - Reports now include structure name and source file +- **Structure Column in CSV** - CSV exports include structure identification +- **Multi-Structure Awareness** - Reports indicate which structure from which file + #### Piano Stool (Half-Sandwich) Complex Recognition - **Problem:** Piano stool complexes like [CpMn(CO)โ‚ƒ] were analyzed as CN=8 (all atoms separately), giving poor CShM values (>15.0) - **Solution:** Automatic pattern detection and geometry-aware analysis @@ -57,10 +84,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### ๐Ÿ”ง Technical Details -**Modified Files:** +**New Files:** +- `src/utils/cifParser.js` - Complete CIF file parser with validation and metadata extraction +- `src/utils/fileParser.test.js` - Extended with 14 new tests for multi-frame XYZ functions + +**Modified Files (Multi-File/CIF Support):** +- `src/utils/fileParser.js` - Added `parseXYZAuto()`, `parseMultiXYZ()`, `isMultiFrameXYZ()`, `countXYZFrames()`, `validateMultiXYZ()` +- `src/hooks/useFileUpload.js` - Rewritten for multi-file/multi-format support with structure state management +- `src/components/FileUploadSection.jsx` - Added structure selector UI with navigation buttons +- `src/services/reportGenerator.js` - Added structure name and format to PDF/CSV reports +- `src/App.js` - Updated to use new structure selection props + +**Modified Files (Piano Stool Support):** - `src/constants/algorithmConstants.js` - Added `PIANO_STOOL_GEOMETRIES` constant - `src/services/coordination/patterns/geometryBuilder.js` - Implemented intelligent geometry filtering +**New Dependencies:** +- `crystcif-parse` (^0.2.9) - CIF file parsing library with fractional-to-Cartesian conversion + **Key Algorithm Changes:** 1. Piano stool patterns detected with 85% confidence 2. Centroid-based coordination number calculation diff --git a/CITATION.cff b/CITATION.cff index 057ce43..98fe5a5 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -10,8 +10,8 @@ authors: repository-code: "https://github.com/HenriqueCSJ/q-shape" url: "https://henriquecsj.github.io/q-shape" license: MIT -version: "v1.4.0" -date-released: "2025-11-25" +version: "v1.5.0" +date-released: "2026-01-09" keywords: - "coordination chemistry" - "continuous shape measures" @@ -26,7 +26,7 @@ identifiers: preferred-citation: type: software title: "Q-Shape - Quantitative Shape Analyzer" - version: "v1.4.0" + version: "v1.5.0" year: 2025 authors: - given-names: Henrique diff --git a/README.md b/README.md index 8760a43..9381627 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ![Q-Shape Logo](https://img.shields.io/badge/Q--Shape-Molecular%20Geometry%20Analysis-blue?style=for-the-badge&logo=react&logoColor=white) -[![Version](https://img.shields.io/badge/version-1.4.0-blue.svg?style=flat-square)](https://github.com/HenriqueCSJ/q-shape/releases/tag/v1.4.0) +[![Version](https://img.shields.io/badge/version-1.5.0-blue.svg?style=flat-square)](https://github.com/HenriqueCSJ/q-shape/releases/tag/v1.5.0) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.17717110.svg)](https://doi.org/10.5281/zenodo.17717110) [![MIT License](https://img.shields.io/badge/License-MIT-green.svg?style=flat-square)](https://choosealicense.com/licenses/mit/) [![Website](https://img.shields.io/website?down_color=red&down_message=offline&style=flat-square&up_color=green&up_message=online&url=https%3A%2F%2Fhenriquecsj.github.io%2Fq-shape)](https://henriquecsj.github.io/q-shape) @@ -46,6 +46,14 @@ โœ… **Comprehensive Metrics** - Bond statistics, angular distortion, quality scoring โœ… **PDF Reports** - Professional output suitable for publication +### File Format Support (v1.5.0) + +โœ… **XYZ Files** - Standard molecular geometry format +โœ… **Multi-Geometry XYZ** - Multiple structures in a single file (trajectory-like format) +โœ… **CIF Files** - Crystallographic Information Files with automatic coordinate conversion +โœ… **Multi-File Upload** - Analyze multiple files simultaneously +โœ… **Structure Navigator** - Easy switching between loaded structures + ### Analysis Modes **Standard Mode** (~5-10 seconds) @@ -70,12 +78,13 @@ Visit **[https://henriquecsj.github.io/q-shape](https://henriquecsj.github.io/q- ### Basic Workflow -1. **Upload** your XYZ file (drag-and-drop or file picker) -2. **Select** metal center (auto-detected or manual selection) -3. **Adjust** coordination sphere radius if needed -4. **Run** analysis (standard or intensive mode) -5. **Visualize** results in 3D and review shape measures -6. **Export** PDF report for your records +1. **Upload** your structure file(s) - XYZ or CIF format (drag-and-drop or file picker) +2. **Select** structure if multiple were loaded (use dropdown or navigation buttons) +3. **Select** metal center (auto-detected or manual selection) +4. **Adjust** coordination sphere radius if needed +5. **Run** analysis (standard or intensive mode) +6. **Visualize** results in 3D and review shape measures +7. **Export** PDF report or CSV data for your records --- @@ -203,16 +212,18 @@ Q-Shape has been validated against SHAPE 2.1 (Fortran reference implementation): ## Input Format -Q-Shape accepts standard XYZ molecular structure files: +Q-Shape accepts multiple molecular structure formats: + +### XYZ Format (Single Structure) ```xyz - + ... ``` -### Example: Octahedral Iron Complex +**Example: Octahedral Iron Complex** ```xyz 7 @@ -226,6 +237,58 @@ O 0.000 0.000 2.100 O 0.000 0.000 -2.100 ``` +### Multi-Geometry XYZ Format (v1.5.0) + +Multiple structures concatenated in a single file: + +```xyz +7 +Structure_A - First geometry +Fe 0.000 0.000 0.000 +O 2.100 0.000 0.000 +... +7 +Structure_B - Second geometry +Fe 0.000 0.000 0.000 +O 2.050 0.000 0.000 +... +``` + +Each structure is automatically detected and can be analyzed separately using the structure navigator. + +### CIF Format (v1.5.0) + +Q-Shape supports Crystallographic Information Files (CIF): + +```cif +data_example_structure +_cell_length_a 10.0 +_cell_length_b 10.0 +_cell_length_c 10.0 +_cell_angle_alpha 90.0 +_cell_angle_beta 90.0 +_cell_angle_gamma 90.0 + +loop_ +_atom_site_label +_atom_site_type_symbol +_atom_site_fract_x +_atom_site_fract_y +_atom_site_fract_z +Fe1 Fe 0.5 0.5 0.5 +O1 O 0.71 0.5 0.5 +... +``` + +**CIF Features:** +- Automatic fractional-to-Cartesian coordinate conversion +- Multi-block CIF files supported (each data block becomes a structure) +- Space group and cell parameter metadata preserved + +### Multiple File Upload (v1.5.0) + +Upload multiple XYZ and/or CIF files at once. All structures are loaded and accessible via the structure navigator dropdown. + **Expected Result**: CShM(Octahedron) โ‰ˆ 0.00-0.10 (perfect geometry) --- @@ -339,7 +402,7 @@ If you use Q-Shape in your research, please cite: **APA:** ``` -Castro Silva Junior, H. (2025). Q-Shape - Quantitative Shape Analyzer (v1.4.0). +Castro Silva Junior, H. (2025). Q-Shape - Quantitative Shape Analyzer (v1.5.0). Zenodo. https://doi.org/10.5281/zenodo.17717110 ``` @@ -348,7 +411,7 @@ Zenodo. https://doi.org/10.5281/zenodo.17717110 @software{qshape2025, author = {Castro Silva Junior, Henrique}, title = {Q-Shape - Quantitative Shape Analyzer}, - version = {1.4.0}, + version = {1.5.0}, year = {2025}, doi = {10.5281/zenodo.17717110}, url = {https://doi.org/10.5281/zenodo.17717110}, diff --git a/package-lock.json b/package-lock.json index 4b6bcbe..05b5435 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,14 @@ { "name": "q-shape", - "version": "1.4.0", + "version": "1.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "q-shape", - "version": "1.4.0", + "version": "1.5.0", "dependencies": { + "crystcif-parse": "^0.2.9", "munkres-js": "^1.2.2", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -5661,6 +5662,19 @@ "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", "license": "MIT" }, + "node_modules/complex.js": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.4.3.tgz", + "integrity": "sha512-UrQVSUur14tNX6tiP4y8T4w4FeJAX3bi2cIv0pu/DTLFNxoq7z2Yh83Vfzztj6Px3X/lubqQ9IrPp7Bpn6p4MQ==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -5849,6 +5863,19 @@ "node": ">=8" } }, + "node_modules/crystcif-parse": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/crystcif-parse/-/crystcif-parse-0.2.9.tgz", + "integrity": "sha512-h2pirCEB2yZLjOVL02uULwmI41leiJlQLUgO5HWYh2r+wt1aKMJ8IGPr74U9H5tNyyc3FF8utYj+Yfh60QrRfQ==", + "license": "MIT", + "dependencies": { + "mathjs": "^7.6.0", + "mendeleev": "^1.2.2" + }, + "bin": { + "validate-cif": "bin/validate.js" + } + }, "node_modules/css-blank-pseudo": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz", @@ -6975,6 +7002,12 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/escape-latex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz", + "integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -9844,6 +9877,12 @@ "node": ">=10" } }, + "node_modules/javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", + "license": "MIT" + }, "node_modules/jest": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", @@ -11188,6 +11227,28 @@ "node": ">= 0.4" } }, + "node_modules/mathjs": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-7.6.0.tgz", + "integrity": "sha512-abywR28hUpKF4at5jE8Ys+Kigk40eKMT5mcBLD0/dtsqjfOLbtzd3WjlRqIopNo7oQ6FME51qph6lb8h/bhpUg==", + "license": "Apache-2.0", + "dependencies": { + "complex.js": "^2.0.11", + "decimal.js": "^10.2.1", + "escape-latex": "^1.2.0", + "fraction.js": "^4.0.12", + "javascript-natural-sort": "^0.7.1", + "seed-random": "^2.2.0", + "tiny-emitter": "^2.1.0", + "typed-function": "^2.0.0" + }, + "bin": { + "mathjs": "bin/cli.js" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/mdn-data": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", @@ -11215,6 +11276,12 @@ "node": ">= 4.0.0" } }, + "node_modules/mendeleev": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/mendeleev/-/mendeleev-1.2.2.tgz", + "integrity": "sha512-F/mZqsr2dt94kddCxj66sAbfEXGDKbICut86ONTeuN9st4u0R2Zc39FrCWYWYuVdzmQlE9y4RFFgcuJ6bg6n+A==", + "license": "ISC" + }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -14517,6 +14584,12 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, + "node_modules/seed-random": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/seed-random/-/seed-random-2.2.0.tgz", + "integrity": "sha512-34EQV6AAHQGhoc0tn/96a9Fsi6v2xdqe/dMUwljGRaFOzR3EgRmECvD0O8vi8X+/uQ50LGHfkNu/Eue5TPKZkQ==", + "license": "MIT" + }, "node_modules/select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -16040,6 +16113,12 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "license": "MIT" }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", + "license": "MIT" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -16318,6 +16397,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typed-function": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-2.1.0.tgz", + "integrity": "sha512-bctQIOqx2iVbWGDGPWwIm18QScpu2XRmkC19D8rQGFsjKSgteq/o1hTZvIG/wuDq8fanpBDrLkLq+aEN/6y5XQ==", + "engines": { + "node": ">= 10" + } + }, "node_modules/typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", diff --git a/package.json b/package.json index a82796c..4533b71 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,11 @@ { "name": "q-shape", - "version": "1.4.0", + "version": "1.5.0", "description": "Quantitative Shape Analyzer for Coordination Geometry Analysis", "homepage": "https://HenriqueCSJ.github.io/q-shape", "private": true, "dependencies": { + "crystcif-parse": "^0.2.9", "munkres-js": "^1.2.2", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/src/App.js b/src/App.js index 743b907..2987cbe 100644 --- a/src/App.js +++ b/src/App.js @@ -10,7 +10,7 @@ import { useThreeScene } from './hooks/useThreeScene'; // Services import { runIntensiveAnalysisAsync } from './services/coordination/intensiveAnalysis'; -import { generatePDFReport, generateCSVReport } from './services/reportGenerator'; +import { generatePDFReport, generateCSVReport, generateBatchPDFReport, generateBatchCSVReport } from './services/reportGenerator'; // Components import FileUploadSection from './components/FileUploadSection'; @@ -18,6 +18,10 @@ import AnalysisControls from './components/AnalysisControls'; import CoordinationSummary from './components/CoordinationSummary'; import Visualization3D from './components/Visualization3D'; import ResultsDisplay from './components/ResultsDisplay'; +import BatchResultsDisplay from './components/BatchResultsDisplay'; + +// Batch Analysis Hook +import useBatchAnalysis from './hooks/useBatchAnalysis'; // --- START: REACT COMPONENT --- export default function CoordinationGeometryAnalyzer() { @@ -39,8 +43,30 @@ export default function CoordinationGeometryAnalyzer() { const canvasRef = useRef(null); const fileInputRef = useRef(null); - // File Upload Hook - const { atoms, fileName, error, uploadMetadata, handleFileUpload } = useFileUpload(); + // File Upload Hook (v1.5.0 - supports multiple files/structures) + const { + atoms, + fileName, + error, + uploadMetadata, + handleFileUpload, + structures, + selectedStructureIndex, + currentStructure, + selectStructure + } = useFileUpload(); + + // Batch Analysis Hook (v1.5.0 - analyze all structures at once) + const { + batchResults, + isAnalyzing: isBatchAnalyzing, + progress: batchProgress, + runBatchAnalysis, + clearResults: clearBatchResults + } = useBatchAnalysis(); + + // Determine if we're in batch mode (multiple structures loaded) + const isBatchMode = structures.length > 1; // Stable callback for radius changes const handleRadiusChange = useCallback((radius, isAuto) => { @@ -239,7 +265,9 @@ export default function CoordinationGeometryAnalyzer() { fileName, analysisMode: analysisParams.mode, intensiveMetadata, - imgData + imgData, + structureName: currentStructure?.name || null, + fileFormat: currentStructure?.format || 'xyz' }); } catch (err) { console.error("Report generation failed:", err); @@ -253,12 +281,53 @@ export default function CoordinationGeometryAnalyzer() { if (!geometryResults || geometryResults.length === 0) return; try { - generateCSVReport({ geometryResults, fileName }); + generateCSVReport({ + geometryResults, + fileName, + structureName: currentStructure?.name || null + }); } catch (err) { console.error("CSV generation failed:", err); setWarnings(prev => [...prev, `CSV export failed: ${err.message}`]); } - }, [geometryResults, fileName]); + }, [geometryResults, fileName, currentStructure]); + + // Batch PDF Report generation + const handleGenerateBatchReport = useCallback(() => { + if (!batchResults || batchResults.length === 0) return; + + try { + generateBatchPDFReport({ + batchResults, + fileName + }); + } catch (err) { + console.error("Batch report generation failed:", err); + setWarnings(prev => [...prev, `Batch report failed: ${err.message}`]); + } + }, [batchResults, fileName]); + + // Batch CSV Export + const handleGenerateBatchCSV = useCallback(() => { + if (!batchResults || batchResults.length === 0) return; + + try { + generateBatchCSVReport({ + batchResults, + fileName + }); + } catch (err) { + console.error("Batch CSV generation failed:", err); + setWarnings(prev => [...prev, `Batch CSV export failed: ${err.message}`]); + } + }, [batchResults, fileName]); + + // Clear batch results when new files are uploaded + useEffect(() => { + if (uploadMetadata?.uploadTime) { + clearBatchResults(); + } + }, [uploadMetadata?.uploadTime, clearBatchResults]); return (
@@ -276,10 +345,10 @@ export default function CoordinationGeometryAnalyzer() { marginTop: '0.5rem', fontFamily: 'monospace' }}> - Version 1.4.0 | Built: November 25, 2025 + Version 1.5.0 | Built: January 2026

- Cite this: Castro Silva Junior, H. (2025). Q-Shape - Quantitative Shape Analyzer (v1.4.0). Zenodo. https://doi.org/10.5281/zenodo.17717110 + Cite this: Castro Silva Junior, H. (2025). Q-Shape - Quantitative Shape Analyzer (v1.5.0). Zenodo. https://doi.org/10.5281/zenodo.17717110

@@ -318,9 +387,87 @@ export default function CoordinationGeometryAnalyzer() { - {atoms.length > 0 && ( + {/* Batch Mode UI - Multiple Structures */} + {isBatchMode && structures.length > 0 && ( +
+
+

+ ๐Ÿ“ฆ Batch Mode: {structures.length} Structures Loaded +

+

+ Multi-geometry file or multiple files detected. Use batch analysis to process all structures at once. +

+
+ + + + {/* Batch Report Buttons */} + {batchResults.length > 0 && ( +
+ + +
+ )} +
+ )} + + {/* Single Structure Mode UI */} + {!isBatchMode && atoms.length > 0 && ( <> { + setExpandedStructure(expandedStructure === index ? null : index); + }; + + // Render loading state + if (isAnalyzing) { + return ( +
+
๐Ÿ”ฌ
+
+ Running Batch Analysis +
+ {progress && ( +
+
+
+
+
+ Analyzing {progress.current} of {progress.total}: {progress.structureName} +
+
+ )} +
+ ); + } + + // Render start analysis prompt + if (batchResults.length === 0 && structures && structures.length > 0) { + return ( +
+
๐Ÿ“Š
+
+ {structures.length} structure{structures.length > 1 ? 's' : ''} loaded +
+ +
+ ); + } + + // Render no results state + if (batchResults.length === 0) { + return ( +
+
๐Ÿ“‚
+ Upload structure files to begin batch analysis +
+ ); + } + + // Count successful analyses + const successfulResults = batchResults.filter(r => !r.error && r.bestGeometry); + + return ( +
+

+ ๐Ÿ“Š Batch Analysis Results +

+ + {/* Summary Statistics */} +
+
+
+ {successfulResults.length} +
+
Analyzed
+
+
+
+ {batchResults.filter(r => r.error).length} +
+
Errors
+
+
+
+ {batchResults.length} +
+
Total
+
+
+ + {/* Summary Table */} +
+ + + + + + + + + + + + + {batchResults.map((result, index) => { + const interpretation = result.bestCShM != null + ? interpretShapeMeasure(result.bestCShM) + : null; + const isExpanded = expandedStructure === index; + + return ( + + !result.error && toggleExpanded(index)} + style={{ + background: result.error + ? '#fef2f2' + : isExpanded + ? '#eff6ff' + : index % 2 === 0 ? '#fff' : '#f9fafb', + cursor: result.error ? 'default' : 'pointer', + transition: 'background 0.2s' + }} + > + + + + + + + + + {/* Expanded Details */} + {isExpanded && !result.error && ( + + + + )} + + ); + })} + +
StructureMetalCNBest GeometryCShMQuality
+ {!result.error && (isExpanded ? 'โ–ผ ' : 'โ–ถ ')} + {result.structureName} + + {result.error ? 'โ€”' : `${result.metalElement} #${result.metalIndex + 1}`} + + {result.error ? 'โ€”' : result.coordinationNumber} + + {result.error ? ( + + โš ๏ธ {result.error} + + ) : ( + + {result.bestGeometry} + + {POINT_GROUPS[result.bestGeometry] || ''} + + + )} + + {result.bestCShM != null ? result.bestCShM.toFixed(4) : 'โ€”'} + + {interpretation ? ( + 80 + ? '#d1fae5' + : interpretation.confidence > 50 + ? '#fef3c7' + : '#fee2e2', + color: interpretation.confidence > 80 + ? '#059669' + : interpretation.confidence > 50 + ? '#d97706' + : '#dc2626' + }}> + {interpretation.text} + + ) : 'โ€”'} +
+
+

+ All Geometries for {result.structureName} +

+
+ {result.geometryResults.slice(0, 10).map((geom, gIdx) => { + const geomInterp = interpretShapeMeasure(geom.shapeMeasure); + return ( +
+ + {gIdx + 1}. {geom.name} + + + {geom.shapeMeasure.toFixed(4)} + +
+ ); + })} +
+ {result.geometryResults.length > 10 && ( +
+ + {result.geometryResults.length - 10} more geometries +
+ )} +
+
+
+ + {/* Re-analyze button */} +
+ +
+
+ ); +} diff --git a/src/components/FileUploadSection.jsx b/src/components/FileUploadSection.jsx index 73abfeb..e9c7f1c 100644 --- a/src/components/FileUploadSection.jsx +++ b/src/components/FileUploadSection.jsx @@ -1,26 +1,199 @@ /** * File Upload Section Component * - * Handles XYZ file upload interface + * Handles XYZ and CIF file upload interface. + * Supports multiple file upload and multi-frame XYZ files. + * Includes structure selector when multiple structures are loaded. */ import React from 'react'; -export default function FileUploadSection({ fileInputRef, onFileUpload }) { +export default function FileUploadSection({ + fileInputRef, + onFileUpload, + structures = [], + selectedStructureIndex = 0, + onSelectStructure, + uploadMetadata, + isBatchMode = false +}) { + const hasMultipleStructures = structures.length > 1; + // In batch mode, don't show individual structure selector (batch analysis handles all structures) + const showStructureSelector = hasMultipleStructures && !isBatchMode; + return (
+ + Supports .xyz (single/multi-frame) and .cif files +
+ + {/* Structure count badge */} + {uploadMetadata && uploadMetadata.structureCount > 1 && ( +
+ + ๐Ÿ“Š {uploadMetadata.structureCount} structures loaded + + {uploadMetadata.fileCount > 1 && ( + + from {uploadMetadata.fileCount} files + + )} + {uploadMetadata.formats && uploadMetadata.formats.length > 1 && ( + + ({uploadMetadata.formats.map(f => f.toUpperCase()).join(', ')}) + + )} +
+ )} + + {/* Structure selector dropdown - hidden in batch mode */} + {showStructureSelector && ( +
+ + + + {/* Quick navigation buttons for many structures */} + {structures.length > 5 && ( +
+ + + + {selectedStructureIndex + 1} / {structures.length} + + + +
+ )} +
+ )} + + {/* Current structure info - only show in single structure mode */} + {!isBatchMode && structures.length > 0 && structures[selectedStructureIndex] && ( +
+ Current: {structures[selectedStructureIndex].name} + {structures[selectedStructureIndex].source && structures[selectedStructureIndex].source !== structures[selectedStructureIndex].name && ( + + (from {structures[selectedStructureIndex].source}) + + )} +
+ )}
); } diff --git a/src/hooks/useBatchAnalysis.js b/src/hooks/useBatchAnalysis.js new file mode 100644 index 0000000..786e74a --- /dev/null +++ b/src/hooks/useBatchAnalysis.js @@ -0,0 +1,234 @@ +/** + * useBatchAnalysis Hook + * + * Manages batch analysis of multiple structures. + * Analyzes all loaded structures and stores results for each. + * + * @returns {Object} Batch analysis state and handlers + */ + +import { useState, useCallback } from 'react'; +import { detectMetalCenter } from '../services/coordination/metalDetector'; +import { detectOptimalRadius } from '../services/coordination/radiusDetector'; +import calculateShapeMeasure from '../services/shapeAnalysis/shapeCalculator'; +import { REFERENCE_GEOMETRIES } from '../constants/referenceGeometries'; + +/** + * Get coordination atoms within radius of metal center + */ +function getCoordinationAtoms(atoms, metalIndex, radius) { + if (!atoms || metalIndex == null || !atoms[metalIndex]) return []; + + const metal = atoms[metalIndex]; + const coordAtoms = []; + + atoms.forEach((atom, i) => { + if (i === metalIndex) return; + + const dx = atom.x - metal.x; + const dy = atom.y - metal.y; + const dz = atom.z - metal.z; + const distance = Math.sqrt(dx * dx + dy * dy + dz * dz); + + if (distance <= radius) { + coordAtoms.push({ atom, index: i, distance }); + } + }); + + // Sort by distance + coordAtoms.sort((a, b) => a.distance - b.distance); + return coordAtoms; +} + +/** + * Calculate shape measures for all reference geometries of a given CN + */ +function calculateAllShapeMeasures(coordAtoms, refGeometries) { + // Extract just the coordinates from coordAtoms + const actualCoords = coordAtoms.map(c => [c.atom.x, c.atom.y, c.atom.z]); + + const results = []; + const geometryNames = Object.keys(refGeometries); + + for (const name of geometryNames) { + const refCoords = refGeometries[name]; + + try { + const { measure, alignedCoords, rotationMatrix } = calculateShapeMeasure( + actualCoords, + refCoords, + 'default', // Use default mode for batch (faster) + null // No progress callback for batch + ); + + // Only include valid results + if (typeof measure === 'number' && isFinite(measure)) { + results.push({ + name, + shapeMeasure: measure, + refCoords, + alignedCoords, + rotationMatrix + }); + } + } catch (error) { + console.warn(`Failed to calculate shape measure for ${name}:`, error.message); + } + } + + return results; +} + +/** + * Analyze a single structure + */ +function analyzeStructure(structure) { + const { atoms, name } = structure; + + // Detect metal center + const metalIndex = detectMetalCenter(atoms); + if (metalIndex == null) { + return { + structureName: name, + error: 'No metal center detected', + metalIndex: null, + metalElement: null, + coordinationNumber: 0, + bestGeometry: null, + bestCShM: null, + geometryResults: [] + }; + } + + const metal = atoms[metalIndex]; + + // Detect optimal radius + const radius = detectOptimalRadius(metal, atoms); + + // Get coordination atoms + const coordAtoms = getCoordinationAtoms(atoms, metalIndex, radius); + const cn = coordAtoms.length; + + if (cn < 2) { + return { + structureName: name, + error: `Insufficient coordination number (CN=${cn})`, + metalIndex, + metalElement: metal.element, + coordinationNumber: cn, + radius, + bestGeometry: null, + bestCShM: null, + geometryResults: [] + }; + } + + // Get reference geometries for this CN + const refGeometries = REFERENCE_GEOMETRIES[cn]; + if (!refGeometries || Object.keys(refGeometries).length === 0) { + return { + structureName: name, + error: `No reference geometries for CN=${cn}`, + metalIndex, + metalElement: metal.element, + coordinationNumber: cn, + radius, + bestGeometry: null, + bestCShM: null, + geometryResults: [] + }; + } + + // Calculate shape measures for all geometries + const geometryResults = calculateAllShapeMeasures(coordAtoms, refGeometries); + + // Sort by shape measure (best first) + geometryResults.sort((a, b) => a.shapeMeasure - b.shapeMeasure); + + const best = geometryResults[0]; + + return { + structureName: name, + metalIndex, + metalElement: metal.element, + coordinationNumber: cn, + radius, + coordAtoms, + bestGeometry: best?.name || null, + bestCShM: best?.shapeMeasure ?? null, + geometryResults, + atoms + }; +} + +export function useBatchAnalysis() { + const [batchResults, setBatchResults] = useState([]); + const [isAnalyzing, setIsAnalyzing] = useState(false); + const [progress, setProgress] = useState(null); + const [error, setError] = useState(null); + + /** + * Run batch analysis on all structures + */ + const runBatchAnalysis = useCallback(async (structures) => { + if (!structures || structures.length === 0) { + setError('No structures to analyze'); + return; + } + + setIsAnalyzing(true); + setError(null); + setBatchResults([]); + + const results = []; + const total = structures.length; + + try { + for (let i = 0; i < structures.length; i++) { + const structure = structures[i]; + + setProgress({ + current: i + 1, + total, + structureName: structure.name, + percentage: Math.round(((i + 1) / total) * 100) + }); + + // Use setTimeout to allow UI updates between structures + await new Promise(resolve => setTimeout(resolve, 10)); + + const result = analyzeStructure(structure); + results.push(result); + } + + setBatchResults(results); + setProgress(null); + + } catch (err) { + console.error('Batch analysis failed:', err); + setError(err.message); + } finally { + setIsAnalyzing(false); + } + }, []); + + /** + * Clear batch results + */ + const clearResults = useCallback(() => { + setBatchResults([]); + setError(null); + setProgress(null); + }, []); + + return { + batchResults, + isAnalyzing, + progress, + error, + runBatchAnalysis, + clearResults + }; +} + +export default useBatchAnalysis; diff --git a/src/hooks/useFileUpload.js b/src/hooks/useFileUpload.js index 385e3d7..d53e388 100644 --- a/src/hooks/useFileUpload.js +++ b/src/hooks/useFileUpload.js @@ -2,43 +2,284 @@ * useFileUpload Hook * * Manages file upload, validation, and parsing for molecular structure files. - * Handles XYZ file format with comprehensive validation and error handling. + * Supports XYZ (single and multi-frame) and CIF file formats. + * Handles multiple file uploads simultaneously. * * @returns {Object} File upload state and handlers - * @returns {Array} atoms - Parsed molecular structure - * @returns {String} fileName - Name of uploaded file (without .xyz extension) + * @returns {Array} structures - Array of parsed Structure objects + * @returns {Array} atoms - Atoms from currently selected structure (backwards compatible) + * @returns {number} selectedStructureIndex - Index of currently selected structure + * @returns {String} fileName - Name(s) of uploaded file(s) * @returns {String|null} error - Error message if upload/parsing failed * @returns {Array} warnings - Array of warning messages + * @returns {Object} uploadMetadata - Metadata about the upload * @returns {Function} handleFileUpload - File upload event handler + * @returns {Function} selectStructure - Select a structure by index * @returns {Function} resetFileState - Reset all file-related state * * @example - * const { atoms, fileName, error, warnings, handleFileUpload } = useFileUpload(); + * const { structures, atoms, handleFileUpload, selectStructure } = useFileUpload(); * - * // In JSX: - * + * // In JSX - supports multiple files and both formats: + * */ -import { useState, useCallback } from 'react'; -import { parseXYZ, validateXYZ } from '../utils/fileParser'; +import { useState, useCallback, useMemo } from 'react'; +import { + parseXYZ, + validateXYZ, + parseXYZAuto, + isMultiFrameXYZ, + validateMultiXYZ +} from '../utils/fileParser'; +import { parseCIF, validateCIF, isCIFContent } from '../utils/cifParser'; import { detectMetalCenter } from '../services/coordination/metalDetector'; import { detectOptimalRadius } from '../services/coordination/radiusDetector'; +/** + * Determines file format from filename extension + * @param {string} filename - Filename with extension + * @returns {'xyz'|'cif'|'unknown'} File format + */ +function getFileFormat(filename) { + const ext = filename.toLowerCase().split('.').pop(); + if (ext === 'xyz') return 'xyz'; + if (ext === 'cif') return 'cif'; + return 'unknown'; +} + export function useFileUpload() { - const [atoms, setAtoms] = useState([]); + // Multi-structure state + const [structures, setStructures] = useState([]); + const [selectedStructureIndex, setSelectedStructureIndex] = useState(0); + + // Legacy state (maintained for backwards compatibility) const [fileName, setFileName] = useState(""); const [error, setError] = useState(null); const [warnings, setWarnings] = useState([]); const [uploadMetadata, setUploadMetadata] = useState(null); + // Computed: atoms from currently selected structure (backwards compatible) + const atoms = useMemo(() => { + if (structures.length === 0) return []; + const selected = structures[selectedStructureIndex]; + return selected?.atoms || []; + }, [structures, selectedStructureIndex]); + + // Computed: current structure info + const currentStructure = useMemo(() => { + if (structures.length === 0) return null; + return structures[selectedStructureIndex] || null; + }, [structures, selectedStructureIndex]); + + /** + * Process a single file content + */ + const processFileContent = useCallback((content, filename) => { + const format = getFileFormat(filename); + const sourceName = filename.replace(/\.(xyz|cif)$/i, ""); + const fileWarnings = []; + + // Auto-detect format if unknown + let detectedFormat = format; + if (format === 'unknown') { + if (isCIFContent(content)) { + detectedFormat = 'cif'; + } else { + detectedFormat = 'xyz'; // Default to XYZ + } + fileWarnings.push(`Unknown file extension, detected as ${detectedFormat.toUpperCase()}`); + } + + if (detectedFormat === 'cif') { + // Validate and parse CIF + const validation = validateCIF(content); + if (!validation.valid) { + throw new Error(`${filename}: ${validation.error}`); + } + if (validation.warnings) { + fileWarnings.push(...validation.warnings.map(w => `${filename}: ${w}`)); + } + + const cifStructures = parseCIF(content, sourceName); + return { structures: cifStructures, warnings: fileWarnings }; + + } else { + // XYZ format - check for multi-frame + if (isMultiFrameXYZ(content)) { + const validation = validateMultiXYZ(content); + if (!validation.valid) { + throw new Error(`${filename}: ${validation.error || validation.errors?.[0]?.error}`); + } + if (validation.warnings) { + fileWarnings.push(...validation.warnings.map(w => `${filename}: ${w}`)); + } + + const xyzStructures = parseXYZAuto(content, sourceName); + return { structures: xyzStructures, warnings: fileWarnings }; + + } else { + // Single-frame XYZ + const validation = validateXYZ(content); + if (!validation.valid) { + throw new Error(`${filename}: ${validation.error}`); + } + if (validation.warnings) { + fileWarnings.push(...validation.warnings.map(w => `${filename}: ${w}`)); + } + + const xyzStructures = parseXYZAuto(content, sourceName); + return { structures: xyzStructures, warnings: fileWarnings }; + } + } + }, []); + + /** + * Handle file upload - supports single or multiple files + */ const handleFileUpload = useCallback((e) => { + const files = e.target.files; + if (!files || files.length === 0) return; + + // Reset state + setError(null); + setWarnings([]); + setStructures([]); + setSelectedStructureIndex(0); + + // Build filename string + const fileNames = Array.from(files).map(f => f.name.replace(/\.(xyz|cif)$/i, "")); + setFileName(fileNames.join(', ')); + + // Process all files + const allStructures = []; + const allWarnings = []; + const readPromises = []; + + Array.from(files).forEach((file) => { + const promise = new Promise((resolve, reject) => { + const reader = new FileReader(); + + reader.onload = (ev) => { + try { + const content = String(ev.target?.result || ""); + const result = processFileContent(content, file.name); + resolve(result); + } catch (err) { + reject(err); + } + }; + + reader.onerror = () => { + reject(new Error(`Failed to read ${file.name}`)); + }; + + reader.readAsText(file); + }); + + readPromises.push(promise); + }); + + // Wait for all files to be processed + Promise.all(readPromises) + .then((results) => { + results.forEach(result => { + allStructures.push(...result.structures); + allWarnings.push(...result.warnings); + }); + + if (allStructures.length === 0) { + throw new Error("No valid structures found in uploaded files"); + } + + setStructures(allStructures); + setWarnings(allWarnings); + setSelectedStructureIndex(0); + + // Auto-detect metal for first structure + const firstStructure = allStructures[0]; + const metalIdx = detectMetalCenter(firstStructure.atoms); + + let optimalRadius = 3.0; + if (metalIdx != null && firstStructure.atoms[metalIdx]) { + optimalRadius = detectOptimalRadius(firstStructure.atoms[metalIdx], firstStructure.atoms); + } + + setUploadMetadata({ + detectedMetalIndex: metalIdx, + suggestedRadius: optimalRadius, + atomCount: firstStructure.atoms.length, + structureCount: allStructures.length, + uploadTime: Date.now(), + fileCount: files.length, + formats: [...new Set(allStructures.map(s => s.format))] + }); + + }) + .catch((err) => { + console.error("File upload error:", err); + setError(err.message); + setStructures([]); + setUploadMetadata(null); + }); + + }, [processFileContent]); + + /** + * Select a structure by index + */ + const selectStructure = useCallback((index) => { + if (index < 0 || index >= structures.length) { + console.warn(`Invalid structure index: ${index}`); + return; + } + + setSelectedStructureIndex(index); + + // Update metadata for the newly selected structure + const selected = structures[index]; + if (selected) { + const metalIdx = detectMetalCenter(selected.atoms); + let optimalRadius = 3.0; + if (metalIdx != null && selected.atoms[metalIdx]) { + optimalRadius = detectOptimalRadius(selected.atoms[metalIdx], selected.atoms); + } + + setUploadMetadata(prev => ({ + ...prev, + detectedMetalIndex: metalIdx, + suggestedRadius: optimalRadius, + atomCount: selected.atoms.length, + currentStructureName: selected.name, + currentStructureFormat: selected.format + })); + } + }, [structures]); + + /** + * Reset all file-related state + */ + const resetFileState = useCallback(() => { + setStructures([]); + setSelectedStructureIndex(0); + setFileName(""); + setError(null); + setWarnings([]); + setUploadMetadata(null); + }, []); + + /** + * Legacy single-file upload handler (for backwards compatibility) + * Use handleFileUpload for new code + */ + const handleSingleFileUpload = useCallback((e) => { const file = e.target.files?.[0]; if (!file) return; // Reset state setError(null); setWarnings([]); - setFileName(file.name.replace(/\.xyz$/i, "")); + setFileName(file.name.replace(/\.(xyz|cif)$/i, "")); const reader = new FileReader(); @@ -46,7 +287,7 @@ export function useFileUpload() { try { const content = String(ev.target?.result || ""); - // Validate XYZ file + // Use legacy XYZ-only path for backwards compatibility const validation = validateXYZ(content); if (!validation.valid) { @@ -57,31 +298,36 @@ export function useFileUpload() { setWarnings(validation.warnings); } - // Parse atoms const parsedAtoms = parseXYZ(content); - setAtoms(parsedAtoms); - // Auto-detect metal center - const metalIdx = detectMetalCenter(parsedAtoms); + // Wrap in structure format + setStructures([{ + name: file.name.replace(/\.xyz$/i, ""), + atoms: parsedAtoms, + format: 'xyz', + frameIndex: 0, + source: file.name + }]); + setSelectedStructureIndex(0); - // Calculate optimal radius if metal found - let optimalRadius = 3.0; // default + const metalIdx = detectMetalCenter(parsedAtoms); + let optimalRadius = 3.0; if (metalIdx != null && parsedAtoms[metalIdx]) { optimalRadius = detectOptimalRadius(parsedAtoms[metalIdx], parsedAtoms); } - // Store metadata for parent component setUploadMetadata({ detectedMetalIndex: metalIdx, suggestedRadius: optimalRadius, atomCount: parsedAtoms.length, + structureCount: 1, uploadTime: Date.now() }); } catch (err) { console.error("File upload error:", err); setError(err.message); - setAtoms([]); + setStructures([]); setUploadMetadata(null); } }; @@ -89,28 +335,30 @@ export function useFileUpload() { reader.onerror = () => { const errorMsg = "Failed to read file - please check file permissions and try again"; setError(errorMsg); - setAtoms([]); + setStructures([]); setUploadMetadata(null); }; reader.readAsText(file); }, []); - const resetFileState = useCallback(() => { - setAtoms([]); - setFileName(""); - setError(null); - setWarnings([]); - setUploadMetadata(null); - }, []); - return { + // Multi-structure API + structures, + selectedStructureIndex, + currentStructure, + selectStructure, + + // Backwards compatible API atoms, fileName, error, warnings, uploadMetadata, + + // Handlers handleFileUpload, + handleSingleFileUpload, // Legacy handler resetFileState }; } diff --git a/src/services/reportGenerator.js b/src/services/reportGenerator.js index e9afcb6..1a7fb8f 100644 --- a/src/services/reportGenerator.js +++ b/src/services/reportGenerator.js @@ -41,6 +41,8 @@ function escapeHtml(str) { * @param {string} params.analysisMode - 'default' or 'intensive' * @param {Object} params.intensiveMetadata - Intensive analysis metadata * @param {string} params.imgData - Base64 encoded 3D visualization image + * @param {string} params.structureName - Name of the structure (for multi-structure files) + * @param {string} params.fileFormat - File format ('xyz' or 'cif') * @returns {void} Opens report in new window */ export function generatePDFReport({ @@ -56,7 +58,9 @@ export function generatePDFReport({ fileName, analysisMode, intensiveMetadata, - imgData + imgData, + structureName = null, + fileFormat = 'xyz' }) { if (!atoms.length || selectedMetal == null || !bestGeometry) { throw new Error('Missing required data for report generation'); @@ -426,11 +430,12 @@ footer strong {

๐Ÿ”ฌ Q-Shape (Quantitative Shape Analyzer)

Coordination Geometry Analysis Report

-

File: ${escapeHtml(fileName)}.xyz

+

File: ${escapeHtml(fileName)}.${fileFormat}

+ ${structureName ? `

Structure: ${escapeHtml(structureName)}

` : ''}

Generated on: ${date}

Analysis Mode: ${analysisMode === 'intensive' ? 'Intensive (High Precision) with Kabsch Alignment' : 'Standard with Improved Kabsch Alignment'}

- Cite this: Castro Silva Junior, H. (2025). Q-Shape - Quantitative Shape Analyzer (v1.4.0). Zenodo. + Cite this: Castro Silva Junior, H. (2025). Q-Shape - Quantitative Shape Analyzer (v1.5.0). Zenodo. https://doi.org/10.5281/zenodo.17717110

@@ -644,10 +649,10 @@ footer strong {