diff --git a/backend/app/routers/sample.py b/backend/app/routers/sample.py index 4f89135860b736c1367a6ff406679be530d286ca..b4afb5066fc56783120e9464c024416b87a02074 100644 --- a/backend/app/routers/sample.py +++ b/backend/app/routers/sample.py @@ -17,6 +17,7 @@ from app.schemas import ( ImageInfo, ResultResponse, ResultCreate, + Results as ProcessingResults, ) from app.models import ( Puck as PuckModel, @@ -374,17 +375,29 @@ def create_result(payload: ResultCreate, db: Session = Depends(get_db)): ) -# @router.get("/results", response_model=list[ResultResponse]) -# def get_results(sample_id: int, result_id: int, db: Session = Depends(get_db)): -# query = db.query(Results) -# -# if sample_id: -# query = query.filter(Results.sample_id == sample_id) -# if result_id: -# query = query.filter(Results.result_id == result_id) -# -# results = query.all() -# if not results: -# raise HTTPException(status_code=404, detail="No results found") -# -# return results +@router.get( + "/processing-results/{sample_id}/{run_id}", response_model=List[ResultResponse] +) +async def get_results_for_run_and_sample( + sample_id: int, run_id: int, db: Session = Depends(get_db) +): + results = ( + db.query(ResultsModel) + .filter(ResultsModel.sample_id == sample_id, ResultsModel.run_id == run_id) + .all() + ) + + if not results: + raise HTTPException(status_code=404, detail="Results not found.") + + formatted_results = [ + ResultResponse( + id=result.id, + sample_id=result.sample_id, + run_id=result.run_id, + result=ProcessingResults(**result.result), + ) + for result in results + ] + + return formatted_results diff --git a/backend/pyproject.toml b/backend/pyproject.toml index f6b6a796df829b1306bcbda129523b3fa56d0509..1d7ea895d5845966082cdd125c9dcce9e465c5b6 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "aareDB" -version = "0.1.0a25" +version = "0.1.0a26" description = "Backend for next gen sample management system" authors = [{name = "Guillaume Gotthard", email = "guillaume.gotthard@psi.ch"}] license = {text = "MIT"} diff --git a/frontend/src/components/ResultGrid.tsx b/frontend/src/components/ResultGrid.tsx index cfc6228a19050f00d2a3937cfe4c6eb7575f6fe3..cd39d8d495bc002289ec81776827f415ceedd28f 100644 --- a/frontend/src/components/ResultGrid.tsx +++ b/frontend/src/components/ResultGrid.tsx @@ -85,6 +85,7 @@ interface TreeRow { id: string; hierarchy: (string | number)[]; type: 'sample' | 'run'; + experimentId?: number; sample_id: number; sample_name?: string; puck_name?: string; @@ -209,6 +210,7 @@ const ResultGrid: React.FC<ResultGridProps> = ({ activePgroup }) => { id: `run-${sample.sample_id}-${run.run_number}`, hierarchy: [sample.sample_id, run.run_number], type: 'run', + experimentId: run.id, sample_id: sample.sample_id, run_number: run.run_number, beamline_parameters: run.beamline_parameters, @@ -307,8 +309,10 @@ const ResultGrid: React.FC<ResultGridProps> = ({ activePgroup }) => { return ( <RunDetails run={params.row} + runId={params.row.experimentId} + sample_id={params.row.sample_id} basePath={basePath} - onHeightChange={(height: number) => handleDetailPanelHeightChange(params.row.id, height)} // Pass callback for dynamic height + onHeightChange={height => handleDetailPanelHeightChange(params.row.id, height)} /> ); } diff --git a/frontend/src/components/RunDetails.tsx b/frontend/src/components/RunDetails.tsx index b0914a3a6e1ab2421da1168beb1319fa8fbc9371..b837854365c1b21352bf38845a78865d15e8b71d 100644 --- a/frontend/src/components/RunDetails.tsx +++ b/frontend/src/components/RunDetails.tsx @@ -1,198 +1,281 @@ import React, { useEffect, useRef, useState } from 'react'; import { - Accordion, - AccordionSummary, - AccordionDetails, - Typography, - Grid, - Modal, - Box + Accordion, AccordionSummary, AccordionDetails, Typography, Grid, Modal, Box } from '@mui/material'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import './SampleImage.css'; +import { DataGridPremium, GridColDef } from "@mui/x-data-grid-premium"; +import { SamplesService } from "../../openapi"; interface RunDetailsProps { - run: ExperimentParameters; + run: TreeRow; + runId: number; + sample_id: number; basePath: string; - onHeightChange?: (height: number) => void; // Callback to notify the parent about height changes + onHeightChange?: (height: number) => void; } -const RunDetails: React.FC<RunDetailsProps> = ({ run, onHeightChange, basePath }) => { - const containerRef = useRef<HTMLDivElement | null>(null); // Ref to track component height + +interface ExperimentParameters { + run_number: number; + id: number; + sample_id: number; + beamline_parameters: BeamlineParameters; + images: Image[]; +} + + +interface ProcessingResults { + pipeline: string; + resolution: number; + unit_cell: string; + spacegroup: string; + rmerge: number; + rmeas: number; + isig: number; + cc: number; + cchalf: number; + completeness: number; + multiplicity: number; + nobs: number; + total_refl: number; + unique_refl: number; + comments?: string | null; +} + +const RunDetails: React.FC<RunDetailsProps> = ({ run, onHeightChange, basePath, runId, sample_id }) => { + const containerRef = useRef<HTMLDivElement | null>(null); const [currentHeight, setCurrentHeight] = useState<number>(0); - const [modalOpen, setModalOpen] = useState<boolean>(false); // For modal state - const [selectedImage, setSelectedImage] = useState<string | null>(null); // Tracks the selected image for the modal + const [modalOpen, setModalOpen] = useState<boolean>(false); + const [selectedImage, setSelectedImage] = useState<string | null>(null); + const [expandedResults, setExpandedResults] = useState(false); + const [processingResult, setProcessingResult] = useState<ProcessingResults[] | null>(null); - const { beamline_parameters, images } = run; - const { synchrotron, beamline, detector } = beamline_parameters; + const {beamline_parameters, images} = run; + const {synchrotron, beamline, detector} = beamline_parameters; + + useEffect(() => { + fetchResults(sample_id, runId); // fetching based on experimentId + }, [runId]); + + const fetchResults = async (sample_id: number, runId: number) => { + try { + const results = await SamplesService.getResultsForRunAndSampleSamplesProcessingResultsSampleIdRunIdGet(sample_id, runId); + + // Explicitly handle nested results + const mappedResults: ProcessingResults[] = results.map((res): ProcessingResults => ({ + pipeline: res.result?.pipeline || 'N/A', + resolution: res.result.resolution ?? 0, + unit_cell: res.result?.unit_cell || 'N/A', + spacegroup: res.result?.spacegroup || 'N/A', + rmerge: res.result?.rmerge ?? 0, + rmeas: res.result?.rmeas ?? 0, + isig: res.result?.isig ?? 0, + cc: res.result?.cc ?? 0, + cchalf: res.result?.cchalf ?? 0, + completeness: res.result?.completeness ?? 0, + multiplicity: res.result?.multiplicity ?? 0, + nobs: res.result?.nobs ?? 0, + total_refl: res.result?.total_refl ?? 0, + unique_refl: res.result?.unique_refl ?? 0, + comments: res.result?.comments || null, + })); + + setProcessingResult(mappedResults); + } catch (error) { + console.error('Error fetching results:', error); + } + }; + + + const resultColumns: GridColDef[] = [ + {field: 'pipeline', headerName: 'Pipeline', flex: 1}, + {field: 'resolution', headerName: 'Resolution (Å)', flex: 1}, + {field: 'unit_cell', headerName: 'Unit Cell (Å)', flex: 1.5}, + {field: 'spacegroup', headerName: 'Spacegroup', flex: 1}, + {field: 'rmerge', headerName: 'Rmerge', flex: 1}, + {field: 'rmeas', headerName: 'Rmeas', flex: 1}, + {field: 'isig', headerName: 'I/sig(I)', flex: 1}, + {field: 'cc', headerName: 'CC', flex: 1}, + {field: 'cchalf', headerName: 'CC(1/2)', flex: 1}, + {field: 'completeness', headerName: 'Completeness (%)', flex: 1}, + {field: 'multiplicity', headerName: 'Multiplicity', flex: 1}, + {field: 'nobs', headerName: 'N obs.', flex: 1}, + {field: 'total_refl', headerName: 'Total Reflections', flex: 1}, + {field: 'unique_refl', headerName: 'Unique Reflections', flex: 1}, + {field: 'comments', headerName: 'Comments', flex: 2}, + ]; - // Calculate and notify the parent about height changes const updateHeight = () => { if (containerRef.current) { const newHeight = containerRef.current.offsetHeight; - if (newHeight !== currentHeight) { + if (newHeight !== currentHeight && onHeightChange) { setCurrentHeight(newHeight); - if (onHeightChange) { - onHeightChange(newHeight); - } + onHeightChange(newHeight); } } }; useEffect(() => { - updateHeight(); // Update height on initial render - }, []); - - useEffect(() => { - // Update height whenever the component content changes const observer = new ResizeObserver(updateHeight); - if (containerRef.current) { - observer.observe(containerRef.current); - } - return () => { - observer.disconnect(); - }; - }, [containerRef]); + if (containerRef.current) observer.observe(containerRef.current); + return () => observer.disconnect(); + }, [containerRef.current, processingResult]); const handleImageClick = (imagePath: string) => { setSelectedImage(imagePath); - setModalOpen(true); // Open the modal when the image is clicked + setModalOpen(true); }; const closeModal = () => { - setSelectedImage(null); // Clear the current image + setSelectedImage(null); setModalOpen(false); }; return ( <div - className="details-panel" // Add the class here - ref={containerRef} // Attach the ref to the main container + className="details-panel" + ref={containerRef} style={{ display: 'flex', + flexDirection: 'column', // Stack children vertically gap: '16px', padding: '16px', border: '1px solid #ccc', borderRadius: '4px', - alignItems: 'flex-start', }} > - {/* Main Details Section */} - <div style={{ flexGrow: 1 }}> - <Typography variant="h6" gutterBottom> - Run {run.run_number} Details - </Typography> - <Typography variant="subtitle1" gutterBottom> - Beamline: {beamline} | Synchrotron: {synchrotron} - </Typography> - - {/* Detector Details Accordion */} - <Accordion> - <AccordionSummary - expandIcon={<ExpandMoreIcon />} - aria-controls="detector-content" - id="detector-header" - > - <Typography> - <strong>Detector Details</strong> - </Typography> - </AccordionSummary> - <AccordionDetails> - <Typography>Manufacturer: {detector?.manufacturer || 'N/A'}</Typography> - <Typography>Model: {detector?.model || 'N/A'}</Typography> - <Typography>Type: {detector?.type || 'N/A'}</Typography> - <Typography> - Beam Center (px): x: {detector?.beamCenterX_px || 'N/A'}, y: {detector?.beamCenterY_px || 'N/A'} - </Typography> - </AccordionDetails> - </Accordion> + {/* Wrap details and images together */} + <div style={{display: 'flex', gap: '16px', alignItems: 'flex-start'}}> + {/* Main Details Section */} + <div style={{flexGrow: 1}}> + <Typography variant="h6" gutterBottom> + Run {run.run_number} Details + </Typography> + <Typography variant="subtitle1" gutterBottom> + Beamline: {beamline} | Synchrotron: {synchrotron} + </Typography> - {/* Beamline Details Accordion */} - <Accordion> - <AccordionSummary - expandIcon={<ExpandMoreIcon />} - aria-controls="beamline-content" - id="beamline-header" - > - <Typography> - <strong>Beamline Details</strong> - </Typography> - </AccordionSummary> - <AccordionDetails> - <Typography>Synchrotron: {beamline_parameters?.synchrotron || 'N/A'}</Typography> - <Typography>Ring mode: {beamline_parameters?.ringMode || 'N/A'}</Typography> - <Typography>Ring current: {beamline_parameters?.ringCurrent_A || 'N/A'}</Typography> - <Typography>Beamline: {beamline_parameters?.beamline || 'N/A'}</Typography> - <Typography>Undulator: {beamline_parameters?.undulator || 'N/A'}</Typography> - <Typography>Undulator gap: {beamline_parameters?.undulatorgap_mm || 'N/A'}</Typography> - <Typography>Focusing optic: {beamline_parameters?.focusingOptic || 'N/A'}</Typography> - <Typography>Monochromator: {beamline_parameters?.monochromator || 'N/A'}</Typography> - </AccordionDetails> - </Accordion> + {/* Detector Details Accordion */} + <Accordion> + <AccordionSummary + expandIcon={<ExpandMoreIcon/>} + aria-controls="detector-content" + id="detector-header" + > + <Typography><strong>Detector Details</strong></Typography> + </AccordionSummary> + <AccordionDetails> + <Typography>Manufacturer: {detector?.manufacturer || 'N/A'}</Typography> + <Typography>Model: {detector?.model || 'N/A'}</Typography> + <Typography>Type: {detector?.type || 'N/A'}</Typography> + <Typography> + Beam Center (px): x: {detector?.beamCenterX_px || 'N/A'}, + y: {detector?.beamCenterY_px || 'N/A'} + </Typography> + </AccordionDetails> + </Accordion> + + {/* Beamline Details Accordion */} + <Accordion> + <AccordionSummary expandIcon={<ExpandMoreIcon/>}> + <Typography><strong>Beamline Details</strong></Typography> + </AccordionSummary> + <AccordionDetails> + <Typography>Synchrotron: {beamline_parameters?.synchrotron || 'N/A'}</Typography> + <Typography>Ring mode: {beamline_parameters?.ringMode || 'N/A'}</Typography> + <Typography>Ring current: {beamline_parameters?.ringCurrent_A || 'N/A'}</Typography> + <Typography>Beamline: {beamline_parameters?.beamline || 'N/A'}</Typography> + <Typography>Undulator: {beamline_parameters?.undulator || 'N/A'}</Typography> + <Typography>Undulator gap: {beamline_parameters?.undulatorgap_mm || 'N/A'}</Typography> + <Typography>Focusing optic: {beamline_parameters?.focusingOptic || 'N/A'}</Typography> + <Typography>Monochromator: {beamline_parameters?.monochromator || 'N/A'}</Typography> + </AccordionDetails> + </Accordion> - {/* Beam Characteristics Accordion */} - <Accordion> - <AccordionSummary - expandIcon={<ExpandMoreIcon />} - aria-controls="beam-content" - id="beam-header" - > - <Typography> - <strong>Beam Characteristics</strong> + {/* Beam Characteristics Accordion */} + <Accordion> + <AccordionSummary expandIcon={<ExpandMoreIcon/>}> + <Typography><strong>Beam Characteristics</strong></Typography> + </AccordionSummary> + <AccordionDetails> + <Typography>Wavelength: {beamline_parameters?.wavelength || 'N/A'}</Typography> + <Typography>Energy: {beamline_parameters?.energy || 'N/A'}</Typography> + <Typography>Transmission: {beamline_parameters?.transmission || 'N/A'}</Typography> + <Typography> + Beam focus (µm): vertical: {beamline_parameters?.beamSizeHeight || 'N/A'}, + horizontal:{' '} + {beamline_parameters?.beamSizeWidth || 'N/A'} + </Typography> + <Typography>Flux at sample + (ph/s): {beamline_parameters?.beamlineFluxAtSample_ph_s || 'N/A'}</Typography> + </AccordionDetails> + </Accordion> + </div> + + {/* Image Section */} + <div style={{width: '900px'}}> + <Typography variant="h6" gutterBottom> + Associated Images + </Typography> + {images && images.length > 0 ? ( + <Grid container spacing={1}> + {images.map((img) => ( + <Grid item xs={4} key={img.id}> + <div + className="image-container" + onClick={() => handleImageClick(`${basePath || ''}${img.filepath}`)} + style={{cursor: 'pointer'}} + > + <img + src={`${basePath || ''}${img.filepath}`} + alt={img.comment || 'Image'} + className="zoom-image" + style={{ + width: '100%', + maxWidth: '100%', + borderRadius: '4px', + }} + /> + </div> + </Grid> + ))} + </Grid> + ) : ( + <Typography variant="body2" color="textSecondary"> + No images available. </Typography> + )} + </div> + </div> + + {/* Processing Results Accordion - Full Width Below */} + <div style={{width: '100%'}}> + <Accordion expanded={expandedResults} onChange={(e, expanded) => setExpandedResults(expanded)}> + <AccordionSummary expandIcon={<ExpandMoreIcon/>}> + <Typography><strong>Processing Results</strong></Typography> </AccordionSummary> - <AccordionDetails> - <Typography>Wavelength: {beamline_parameters?.wavelength || 'N/A'}</Typography> - <Typography>Energy: {beamline_parameters?.energy || 'N/A'}</Typography> - <Typography>Transmission: {beamline_parameters?.transmission || 'N/A'}</Typography> - <Typography> - Beam focus (µm): vertical: {beamline_parameters?.beamSizeHeight || 'N/A'}, horizontal:{' '} - {beamline_parameters?.beamSizeWidth || 'N/A'} - </Typography> - <Typography>Flux at sample (ph/s): {beamline_parameters?.beamlineFluxAtSample_ph_s || 'N/A'}</Typography> + <AccordionDetails style={{width: '100%', overflowX: 'auto'}}> + {processingResult ? ( + <div style={{width: '100%'}}> + <DataGridPremium + rows={processingResult.map((res, idx) => ({id: idx, ...res}))} + columns={resultColumns} + autoHeight + hideFooter + columnVisibilityModel={{id: false}} + disableColumnResize={false} + /> + </div> + ) : ( + <Typography variant="body2" color="textSecondary">Loading results...</Typography> + )} </AccordionDetails> </Accordion> </div> - {/* Image Section */} - <div style={{ width: '900px' }}> - <Typography variant="h6" gutterBottom> - Associated Images - </Typography> - {images && images.length > 0 ? ( - <Grid container spacing={1}> - {images.map((img) => ( - <Grid item xs={4} key={img.id}> - <div - className="image-container" - onClick={() => handleImageClick(`${basePath || ''}${img.filepath}`)} // Open modal with image - style={{ - cursor: 'pointer', - }} - > - <img - src={`${basePath || ''}${img.filepath}`} // Ensure basePath - alt={img.comment || 'Image'} - className="zoom-image" - style={{ - width: '100%', // Ensure the image takes the full width of its container - maxWidth: '100%', // Prevent any overflow - borderRadius: '4px', - }} - /> - </div> - </Grid> - ))} - </Grid> - ) : ( - <Typography variant="body2" color="textSecondary"> - No images available. - </Typography> - )} - </div> - {/* Modal for Zoomed Image */} <Modal open={modalOpen} onClose={closeModal}> <Box @@ -224,5 +307,4 @@ const RunDetails: React.FC<RunDetailsProps> = ({ run, onHeightChange, basePath } </div> ); }; - export default RunDetails; \ No newline at end of file diff --git a/testfunctions.ipynb b/testfunctions.ipynb index 093c6a53baf1abf7226629db195fe4574e48a358..44eacb0b4012ee2a10b2404f46c1cdc8f60e9d57 100644 --- a/testfunctions.ipynb +++ b/testfunctions.ipynb @@ -3,8 +3,8 @@ { "metadata": { "ExecuteTime": { - "end_time": "2025-03-17T10:32:07.119518Z", - "start_time": "2025-03-17T10:32:06.622836Z" + "end_time": "2025-03-17T15:41:26.497650Z", + "start_time": "2025-03-17T15:41:25.771778Z" } }, "cell_type": "code", @@ -385,8 +385,8 @@ { "metadata": { "ExecuteTime": { - "end_time": "2025-03-17T10:32:10.718097Z", - "start_time": "2025-03-17T10:32:10.716192Z" + "end_time": "2025-03-17T15:41:47.722623Z", + "start_time": "2025-03-17T15:41:47.720727Z" } }, "cell_type": "code", @@ -736,8 +736,8 @@ { "metadata": { "ExecuteTime": { - "end_time": "2025-03-17T10:46:08.211213Z", - "start_time": "2025-03-17T10:46:08.193139Z" + "end_time": "2025-03-17T15:42:07.300875Z", + "start_time": "2025-03-17T15:42:07.279495Z" } }, "cell_type": "code", @@ -750,12 +750,12 @@ "from aareDBclient.rest import ApiException\n", "\n", "# Your actual sample and experiment IDs\n", - "sample_id = 123 # Replace with valid IDs\n", + "sample_id = sample_id # Replace with valid IDs\n", "run_id = 1 # Replace with valid run_id\n", "\n", "# Create random Results payload\n", "results_data = Results(\n", - " pipeline=\"autoproc\",\n", + " pipeline=\"fastproc\",\n", " resolution=round(random.uniform(1.0, 4.0), 2),\n", " unit_cell=f\"{random.uniform(20, 120):.2f}, {random.uniform(20, 120):.2f}, \"\n", " f\"{random.uniform(20, 120):.2f}, {random.uniform(60, 120):.2f}, \"\n", @@ -804,9 +804,9 @@ "output_type": "stream", "text": [ "DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): 127.0.0.1:8000\n", - "/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host '127.0.0.1'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings\n", + "/Users/gotthardg/PycharmProjects/aaredb/.venv/lib/python3.12/site-packages/urllib3/connectionpool.py:1097: InsecureRequestWarning: Unverified HTTPS request is being made to host '127.0.0.1'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings\n", " warnings.warn(\n", - "DEBUG:urllib3.connectionpool:https://127.0.0.1:8000 \"POST /samples/processing-results HTTP/1.1\" 200 369\n" + "DEBUG:urllib3.connectionpool:https://127.0.0.1:8000 \"POST /samples/processing-results HTTP/1.1\" 200 367\n" ] }, { @@ -814,11 +814,11 @@ "output_type": "stream", "text": [ "API call successful:\n", - "ResultResponse(id=2, sample_id=123, run_id=1, result=Results(pipeline='autoproc', resolution=1.94, unit_cell='23.86, 89.07, 37.39, 63.99, 88.77, 81.42', spacegroup='P41212', rmerge=0.072, rmeas=0.07, isig=29.58, cc=0.758, cchalf=0.915, completeness=93.12, multiplicity=6.1, nobs=279922, total_refl=83994, unique_refl=47041, comments='Random auto-generated test entry'))\n" + "ResultResponse(id=4, sample_id=247, run_id=1, result=Results(pipeline='fastproc', resolution=3.93, unit_cell='56.27, 40.58, 26.64, 93.74, 90.52, 99.53', spacegroup='C2', rmerge=0.097, rmeas=0.13, isig=21.29, cc=0.989, cchalf=0.802, completeness=98.46, multiplicity=6.35, nobs=273232, total_refl=217336, unique_refl=11189, comments='Random auto-generated test entry'))\n" ] } ], - "execution_count": 9 + "execution_count": 3 }, { "metadata": {