Add validations and logging for puck beamtime assignment.

Introduced checks to prevent reassigning beamtime if puck samples have recorded events. Updated logging in beamline-related methods to provide more insight. Simplified data structure updates for dewars, pucks, and samples, ensuring consistency with beamtime assignments.
This commit is contained in:
GotthardG 2025-05-09 13:51:01 +02:00
parent 6a0953c913
commit 707c98c5ce
9 changed files with 303 additions and 114 deletions

View File

@ -709,9 +709,18 @@ dewar_to_beamtime = {
for dewar in dewars # Or use actual beamtime ids for dewar in dewars # Or use actual beamtime ids
} }
# Update dewars and their pucks with consistent beamtime
for dewar in dewars: for dewar in dewars:
dewar.beamtime_id = dewar_to_beamtime[dewar.id] assigned_beamtime_obj = next(
b for b in beamtimes if b.id == dewar_to_beamtime[dewar.id]
)
dewar.beamtimes = [assigned_beamtime_obj]
for puck in pucks:
assigned_beamtime_obj = next(
b for b in beamtimes if b.id == dewar_to_beamtime[puck.dewar_id]
)
puck.beamtimes = [assigned_beamtime_obj]
for puck in pucks: for puck in pucks:
dewar_id = puck.dewar_id # Assuming puck has dewar_id dewar_id = puck.dewar_id # Assuming puck has dewar_id

View File

@ -51,6 +51,8 @@ from app.crud import (
) )
from app.routers.auth import get_current_user from app.routers.auth import get_current_user
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
dewar_router = APIRouter() dewar_router = APIRouter()
@ -599,6 +601,19 @@ async def assign_beamtime_to_dewar(
if not dewar: if not dewar:
raise HTTPException(status_code=404, detail="Dewar not found") raise HTTPException(status_code=404, detail="Dewar not found")
# Check if any sample (in any puck on this dewar) has sample events
for puck in dewar.pucks:
for sample in puck.samples:
sample_event_exists = (
db.query(SampleEvent).filter(SampleEvent.sample_id == sample.id).first()
)
if sample_event_exists:
raise HTTPException(
status_code=400,
detail="Cannot change beamtime:"
"at least one sample has events recorded.",
)
# Find the Beamtime instance, if not unassigning # Find the Beamtime instance, if not unassigning
beamtime = ( beamtime = (
db.query(BeamtimeModel).filter(BeamtimeModel.id == beamtime_id).first() db.query(BeamtimeModel).filter(BeamtimeModel.id == beamtime_id).first()
@ -609,9 +624,7 @@ async def assign_beamtime_to_dewar(
if beamtime_id == 0: if beamtime_id == 0:
dewar.beamtimes = [] dewar.beamtimes = []
else: else:
dewar.beamtimes = [ dewar.beamtimes = [beamtime]
beamtime
] # assign one; append if you want to support multiple
db.commit() db.commit()
db.refresh(dewar) db.refresh(dewar)
@ -621,15 +634,11 @@ async def assign_beamtime_to_dewar(
else: else:
puck.beamtimes = [beamtime] puck.beamtimes = [beamtime]
for sample in puck.samples: for sample in puck.samples:
has_sample_event = ( # Can assume all have no events because of previous check
db.query(SampleEvent).filter(SampleEvent.sample_id == sample.id).count() if beamtime_id == 0:
> 0 sample.beamtimes = []
) else:
if not has_sample_event: sample.beamtimes = [beamtime]
if beamtime_id == 0:
sample.beamtimes = []
else:
sample.beamtimes = [beamtime]
db.commit() db.commit()
return {"status": "success", "dewar_id": dewar.id, "beamtime_id": beamtime_id} return {"status": "success", "dewar_id": dewar.id, "beamtime_id": beamtime_id}
@ -746,6 +755,7 @@ async def get_single_shipment(id: int, db: Session = Depends(get_db)):
operation_id="get_dewars_by_beamtime", operation_id="get_dewars_by_beamtime",
) )
async def get_dewars_by_beamtime(beamtime_id: int, db: Session = Depends(get_db)): async def get_dewars_by_beamtime(beamtime_id: int, db: Session = Depends(get_db)):
logger.info(f"get_dewars_by_beamtime called with beamtime_id={beamtime_id}")
beamtime = ( beamtime = (
db.query(BeamtimeModel) db.query(BeamtimeModel)
.options(joinedload(BeamtimeModel.dewars)) .options(joinedload(BeamtimeModel.dewars))
@ -753,5 +763,9 @@ async def get_dewars_by_beamtime(beamtime_id: int, db: Session = Depends(get_db)
.first() .first()
) )
if not beamtime: if not beamtime:
logger.warning(f"Beamtime {beamtime_id} not found")
raise HTTPException(status_code=404, detail="Beamtime not found") raise HTTPException(status_code=404, detail="Beamtime not found")
logger.info(
f"Returning {len(beamtime.dewars)} dewars: {[d.id for d in beamtime.dewars]}"
)
return beamtime.dewars return beamtime.dewars

View File

@ -665,13 +665,25 @@ async def get_pucks_by_slot(slot_identifier: str, db: Session = Depends(get_db))
@router.patch("/puck/{puck_id}/assign-beamtime", operation_id="assignPuckToBeamtime") @router.patch("/puck/{puck_id}/assign-beamtime", operation_id="assignPuckToBeamtime")
async def assign_beamtime_to_puck( async def assign_beamtime_to_puck(
puck_id: int, puck_id: int,
beamtime_id: int, # expects ?beamtime_id=123 in the query beamtime_id: int,
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
puck = db.query(PuckModel).filter(PuckModel.id == puck_id).first() puck = db.query(PuckModel).filter(PuckModel.id == puck_id).first()
if not puck: if not puck:
raise HTTPException(status_code=404, detail="Puck not found") raise HTTPException(status_code=404, detail="Puck not found")
# Check if any sample in this puck has sample events
for sample in puck.samples:
sample_event_exists = (
db.query(SampleEvent).filter(SampleEvent.sample_id == sample.id).first()
)
if sample_event_exists:
raise HTTPException(
status_code=400,
detail="Cannot change beamtime:"
"at least one sample has events recorded.",
)
beamtime = ( beamtime = (
db.query(BeamtimeModel).filter(BeamtimeModel.id == beamtime_id).first() db.query(BeamtimeModel).filter(BeamtimeModel.id == beamtime_id).first()
if beamtime_id if beamtime_id
@ -681,22 +693,15 @@ async def assign_beamtime_to_puck(
if beamtime_id == 0: if beamtime_id == 0:
puck.beamtimes = [] puck.beamtimes = []
else: else:
puck.beamtimes = [ puck.beamtimes = [beamtime]
beamtime
] # or use .append(beamtime) if you want to support multiple
db.commit() db.commit()
db.refresh(puck) db.refresh(puck)
# Update samples as well
for sample in puck.samples: for sample in puck.samples:
has_sample_event = ( if beamtime_id == 0:
db.query(SampleEvent).filter(SampleEvent.sample_id == sample.id).count() > 0 sample.beamtimes = []
) else:
if not has_sample_event: sample.beamtimes = [beamtime]
if beamtime_id == 0:
sample.beamtimes = []
else:
sample.beamtimes = [beamtime]
db.commit() db.commit()
return {"status": "success", "puck_id": puck.id, "beamtime_id": beamtime_id} return {"status": "success", "puck_id": puck.id, "beamtime_id": beamtime_id}
@ -707,6 +712,7 @@ async def assign_beamtime_to_puck(
operation_id="get_pucks_by_beamtime", operation_id="get_pucks_by_beamtime",
) )
async def get_pucks_by_beamtime(beamtime_id: int, db: Session = Depends(get_db)): async def get_pucks_by_beamtime(beamtime_id: int, db: Session = Depends(get_db)):
logger.info(f"get_pucks_by_beamtime called with beamtime_id={beamtime_id}")
beamtime = ( beamtime = (
db.query(BeamtimeModel) db.query(BeamtimeModel)
.options(joinedload(BeamtimeModel.pucks)) # eager load pucks .options(joinedload(BeamtimeModel.pucks)) # eager load pucks
@ -714,5 +720,9 @@ async def get_pucks_by_beamtime(beamtime_id: int, db: Session = Depends(get_db))
.first() .first()
) )
if not beamtime: if not beamtime:
logger.warning(f"Beamtime {beamtime_id} not found")
raise HTTPException(status_code=404, detail="Beamtime not found") raise HTTPException(status_code=404, detail="Beamtime not found")
logger.info(
f"Returning {len(beamtime.pucks)} pucks: {[p.id for p in beamtime.pucks]}"
)
return beamtime.pucks return beamtime.pucks

View File

@ -168,8 +168,8 @@ async def lifespan(app: FastAPI):
load_slots_data(db) load_slots_data(db)
else: # dev or test environments else: # dev or test environments
print(f"{environment.capitalize()} environment: Regenerating database.") print(f"{environment.capitalize()} environment: Regenerating database.")
# Base.metadata.drop_all(bind=engine) Base.metadata.drop_all(bind=engine)
# Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
# from sqlalchemy.engine import reflection # from sqlalchemy.engine import reflection
# from app.models import ExperimentParameters # adjust the import as needed # from app.models import ExperimentParameters # adjust the import as needed
# inspector = reflection.Inspector.from_engine(engine) # inspector = reflection.Inspector.from_engine(engine)

View File

@ -84,8 +84,31 @@ const App: React.FC = () => {
<Routes> <Routes>
<Route path="/login" element={<LoginView />} /> <Route path="/login" element={<LoginView />} />
<Route path="/" element={<ProtectedRoute element={<HomePage />} />} /> <Route path="/" element={<ProtectedRoute element={<HomePage />} />} />
<Route path="/shipments" element={<ProtectedRoute element={<ShipmentView pgroups={pgroups} activePgroup={activePgroup} />} />} /> <Route path="/shipments"
<Route path="/planning" element={<ProtectedRoute element={<PlanningView />} />} /> element={
<ProtectedRoute
element={
<ShipmentView
pgroups={pgroups}
activePgroup={activePgroup}
/>
}
/>
}
/>
<Route path="/planning"
element={
<ProtectedRoute
element={
<PlanningView
pgroups={pgroups}
activePgroup={activePgroup}
onPgroupChange={handlePgroupChange}
/>
}
/>
}
/>
<Route <Route
path="/results/:beamtimeId" path="/results/:beamtimeId"
element={ element={

View File

@ -5,6 +5,7 @@ import timeGridPlugin from '@fullcalendar/timegrid';
import interactionPlugin from '@fullcalendar/interaction'; import interactionPlugin from '@fullcalendar/interaction';
import '../styles/Calendar.css'; import '../styles/Calendar.css';
import { BeamtimesService, DewarsService, PucksService } from '../../openapi'; import { BeamtimesService, DewarsService, PucksService } from '../../openapi';
import Chip from '@mui/material/Chip'
const beamlineColors: { [key: string]: string } = { const beamlineColors: { [key: string]: string } = {
X06SA: '#FF5733', X06SA: '#FF5733',
@ -18,8 +19,15 @@ interface CustomEvent extends EventInput {
beamtime_shift: string; beamtime_shift: string;
beamtime_id?: number; beamtime_id?: number;
isSubmitted?: boolean; isSubmitted?: boolean;
activePgroup?: string;
pgroups?: string;
} }
interface CalendarProps {
activePgroup: string;
}
const experimentModes = ['SDU-Scheduled', 'SDU-queued', 'Remote', 'In-person']; const experimentModes = ['SDU-Scheduled', 'SDU-queued', 'Remote', 'In-person'];
const darkenColor = (color: string, percent: number): string => { const darkenColor = (color: string, percent: number): string => {
@ -34,7 +42,7 @@ const darkenColor = (color: string, percent: number): string => {
return `#${newColor}`; return `#${newColor}`;
}; };
const Calendar: React.FC = () => { const Calendar = ({ activePgroup }: CalendarProps) => {
const [events, setEvents] = useState<CustomEvent[]>([]); const [events, setEvents] = useState<CustomEvent[]>([]);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [fetchError, setFetchError] = useState<string | null>(null); const [fetchError, setFetchError] = useState<string | null>(null);
@ -59,6 +67,7 @@ const Calendar: React.FC = () => {
setFetchError(null); setFetchError(null);
try { try {
const beamtimes = await BeamtimesService.getMyBeamtimesProtectedBeamtimesMyBeamtimesGet(); const beamtimes = await BeamtimesService.getMyBeamtimesProtectedBeamtimesMyBeamtimesGet();
console.log('Loaded beamtimes:', beamtimes);
const grouped: { [key: string]: any[] } = {}; const grouped: { [key: string]: any[] } = {};
beamtimes.forEach((beamtime: any) => { beamtimes.forEach((beamtime: any) => {
const key = `${beamtime.start_date}|${beamtime.beamline}|${beamtime.pgroups}`; const key = `${beamtime.start_date}|${beamtime.beamline}|${beamtime.pgroups}`;
@ -70,8 +79,9 @@ const Calendar: React.FC = () => {
const shifts = group.map((bt: any) => bt.shift).join(" + "); const shifts = group.map((bt: any) => bt.shift).join(" + ");
const ids = group.map((bt: any) => bt.id); const ids = group.map((bt: any) => bt.id);
const first = group[0]; const first = group[0];
console.log(`[DEBUG] pgroups: ${first.pgroups}`); // Ensure the value of pgroups here is correct
return { return {
id: `${first.beamline}-${first.start_date}-${first.pgroups}`, // ensure uniqueness id: `${first.beamline}-${first.start_date}-${first.pgroups}`,
title: `${first.beamline}: ${shifts}`, title: `${first.beamline}: ${shifts}`,
start: first.start_date, start: first.start_date,
end: first.end_date, end: first.end_date,
@ -82,6 +92,9 @@ const Calendar: React.FC = () => {
borderColor: '#000', borderColor: '#000',
textColor: '#fff', textColor: '#fff',
beamtimes: group, beamtimes: group,
extendedProps: {
pgroups: first.pgroups, // Check that this is a valid, comma-separated string
},
}; };
}); });
setEvents(formattedEvents); setEvents(formattedEvents);
@ -89,6 +102,7 @@ const Calendar: React.FC = () => {
// Fetch associations for all // Fetch associations for all
const assoc: { [id: string]: { dewars: string[]; pucks: string[] } } = {}; const assoc: { [id: string]: { dewars: string[]; pucks: string[] } } = {};
console.log('Fetched associations after loading events:', assoc);
await Promise.all( await Promise.all(
Object.values(grouped).map(async (group) => { Object.values(grouped).map(async (group) => {
@ -103,6 +117,8 @@ const Calendar: React.FC = () => {
DewarsService.getDewarsByBeamtime(beamtimeId), DewarsService.getDewarsByBeamtime(beamtimeId),
PucksService.getPucksByBeamtime(beamtimeId), PucksService.getPucksByBeamtime(beamtimeId),
]); ]);
console.log(`Dewars for beamtime ${beamtimeId}:`, dewars);
console.log(`Pucks for beamtime ${beamtimeId}:`, pucks);
dewars.forEach((d: any) => dewarsSet.add(d.id)); dewars.forEach((d: any) => dewarsSet.add(d.id));
pucks.forEach((p: any) => pucksSet.add(p.id)); pucks.forEach((p: any) => pucksSet.add(p.id));
}) })
@ -115,8 +131,10 @@ const Calendar: React.FC = () => {
}; };
}) })
); );
console.log("Final eventAssociations:", assoc);
setEventAssociations(assoc); setEventAssociations(assoc);
} catch (error) { } catch (error) {
setFetchError('Failed to load beamtime data. Please try again later.'); setFetchError('Failed to load beamtime data. Please try again later.');
setEvents([]); setEvents([]);
@ -250,39 +268,38 @@ const Calendar: React.FC = () => {
? eventInfo.event.extendedProps.beamtimes.length ? eventInfo.event.extendedProps.beamtimes.length
: 1; : 1;
const minHeight = beamtimesInGroup * 26; const minHeight = beamtimesInGroup * 26;
const beamline = eventInfo.event.extendedProps.beamline || 'Unknown'; const beamline = eventInfo.event.extendedProps.beamline || 'Unknown';
const isSelected = selectedEventId === eventInfo.event.id; const isSelected = selectedEventId === eventInfo.event.id;
const isSubmitted = eventInfo.event.extendedProps.isSubmitted; const isSubmitted = eventInfo.event.extendedProps.isSubmitted;
const assoc = eventAssociations[eventInfo.event.id] || { dewars: [], pucks: [] }; const assoc = eventAssociations[eventInfo.event.id] || { dewars: [], pucks: [] };
const backgroundColor = isSubmitted const backgroundColor = isSubmitted
? darkenColor(beamlineColors[beamline] || beamlineColors.Unknown, -20) ? darkenColor(beamlineColors[beamline] || beamlineColors.Unknown, -20)
: isSelected : isSelected
? '#FFD700' ? '#FFD700'
: (beamlineColors[beamline] || beamlineColors.Unknown); : (beamlineColors[beamline] || beamlineColors.Unknown);
return (
return ( <div
<div style={{
style={{ backgroundColor: backgroundColor,
backgroundColor: backgroundColor, color: 'white',
color: 'white', border: isSelected ? '2px solid black' : 'none',
border: isSelected ? '2px solid black' : 'none', borderRadius: '3px',
borderRadius: '3px', display: 'flex',
display: 'flex', justifyContent: 'space-between',
justifyContent: 'space-between', alignItems: 'center',
alignItems: 'center', height: '100%',
height: '100%', width: '100%',
width: '100%', cursor: 'pointer',
cursor: 'pointer', overflow: 'hidden',
overflow: 'hidden', boxSizing: 'border-box',
boxSizing: 'border-box', padding: '0 6px',
padding: '0 6px', minHeight: `${minHeight}px`,
minHeight: `${minHeight}px`, }}
}} >
>
<span style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}> <span style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{eventInfo.event.title} {eventInfo.event.title}
</span> </span>
<span style={{ display: 'flex', alignItems: 'center', gap: 6, marginLeft: 8 }}> <span style={{ display: 'flex', alignItems: 'center', gap: 6, marginLeft: 8 }}>
<span title="Dewars" style={{ display: 'flex', alignItems: 'center', fontSize: 13 }}> <span title="Dewars" style={{ display: 'flex', alignItems: 'center', fontSize: 13 }}>
🧊 🧊
<span style={{ <span style={{
@ -311,13 +328,32 @@ const Calendar: React.FC = () => {
textAlign: 'center' textAlign: 'center'
}}>{assoc.pucks.length}</span> }}>{assoc.pucks.length}</span>
</span> </span>
{eventInfo.event.extendedProps?.pgroups && eventInfo.event.extendedProps.pgroups.split(',')
.map((pgroup: string) => (
<Chip
key={pgroup.trim()}
label={pgroup.trim()}
size="small"
sx={{
marginLeft: 0.5,
marginRight: 0.5,
backgroundColor: pgroup.trim() === activePgroup ? '#19d238' : '#b0b0b0',
color: pgroup.trim() === activePgroup ? 'white' : 'black',
fontWeight: 'bold',
borderRadius: '8px',
height: '20px',
fontSize: '12px',
boxShadow: '0px 1px 3px rgba(0, 0, 0, 0.2)',
mr: 1,
mb: 1,
}}
/>
))
}
</span> </span>
</div> </div>
); );
}; };
// Used in Dewar/Puck assign status reporting
function getAssignedEventForDewar(dewarId: string) { function getAssignedEventForDewar(dewarId: string) {
return Object.entries(eventAssociations).find(([eid, assoc]) => return Object.entries(eventAssociations).find(([eid, assoc]) =>
assoc.dewars.includes(dewarId) assoc.dewars.includes(dewarId)

View File

@ -39,13 +39,13 @@ interface ProcessingResults {
resolution: number; resolution: number;
unit_cell: string; unit_cell: string;
spacegroup: string; spacegroup: string;
rmerge: number; rmerge: CCPoint[];
rmeas: number; rmeas: CCPoint[];
isig: number; isig: CCPoint[];
cc: CCPoint[]; cc: CCPoint[];
cchalf: CCPoint[]; cchalf: CCPoint[];
completeness: number; completeness: CCPoint[];
multiplicity: number; multiplicity: CCPoint[];
nobs: number; nobs: number;
total_refl: number; total_refl: number;
unique_refl: number; unique_refl: number;
@ -81,13 +81,13 @@ const RunDetails: React.FC<RunDetailsProps> = ({ run, onHeightChange, basePath,
resolution: res.result?.resolution ?? 0, resolution: res.result?.resolution ?? 0,
unit_cell: res.result?.unit_cell || 'N/A', unit_cell: res.result?.unit_cell || 'N/A',
spacegroup: res.result?.spacegroup || 'N/A', spacegroup: res.result?.spacegroup || 'N/A',
rmerge: res.result?.rmerge ?? 0, rmerge: res.result?.rmerge || [],
rmeas: res.result?.rmeas ?? 0, rmeas: res.result?.rmeas || [],
isig: res.result?.isig ?? 0, isig: res.result?.isig || [],
cc: res.result?.cc || [], cc: res.result?.cc || [],
cchalf: res.result?.cchalf || [], cchalf: res.result?.cchalf || [],
completeness: res.result?.completeness ?? 0, completeness: res.result?.completeness || [],
multiplicity: res.result?.multiplicity ?? 0, multiplicity: res.result?.multiplicity || [],
nobs: res.result?.nobs ?? 0, nobs: res.result?.nobs ?? 0,
total_refl: res.result?.total_refl ?? 0, total_refl: res.result?.total_refl ?? 0,
unique_refl: res.result?.unique_refl ?? 0, unique_refl: res.result?.unique_refl ?? 0,
@ -111,15 +111,45 @@ const RunDetails: React.FC<RunDetailsProps> = ({ run, onHeightChange, basePath,
{ field: 'resolution', headerName: 'Resolution (Å)', flex: 1 }, { field: 'resolution', headerName: 'Resolution (Å)', flex: 1 },
{ field: 'unit_cell', headerName: 'Unit Cell (Å)', flex: 1.5 }, { field: 'unit_cell', headerName: 'Unit Cell (Å)', flex: 1.5 },
{ field: 'spacegroup', headerName: 'Spacegroup', flex: 1 }, { field: 'spacegroup', headerName: 'Spacegroup', flex: 1 },
{ field: 'rmerge', headerName: 'Rmerge', flex: 1 }, {
{ field: 'rmeas', headerName: 'Rmeas', flex: 1 }, field: 'rmerge',
{ field: 'isig', headerName: 'I/sig(I)', flex: 1 }, headerName: 'Rmerge',
flex: 1,
valueGetter: (params: GridValueGetterParams<ProcessingResults, string>) =>
params.row?.rmerge
? Array.isArray(params.row.rmerge)
? params.row.rmerge.map((value: CCPoint) => `${value.value.toFixed(2)}@${value.resolution.toFixed(2)}`).join(', ')
: params.row.rmerge.toFixed(2)
: 'N/A',
},
{
field: 'rmeas',
headerName: 'Rmeas',
flex: 1,
valueGetter: (params: GridValueGetterParams<ProcessingResults, string>) =>
params.row?.rmeas
? Array.isArray(params.row.rmeas)
? params.row.rmeas.map((value: CCPoint) => `${value.value.toFixed(2)}@${value.resolution.toFixed(2)}`).join(', ')
: params.row.rmeas.toFixed(2)
: 'N/A',
},
{
field: 'isig',
headerName: 'I/sig(I)',
flex: 1,
valueGetter: (params: GridValueGetterParams<ProcessingResults, string>) =>
params.row?.isig
? Array.isArray(params.row.isig)
? params.row.isig.map((value: CCPoint) => `${value.value.toFixed(2)}@${value.resolution.toFixed(2)}`).join(', ')
: params.row.isig.toFixed(2)
: 'N/A',
},
{ {
field: 'cc', field: 'cc',
headerName: 'CC', headerName: 'CC',
flex: 1, flex: 1,
valueGetter: (params: GridValueGetterParams<ProcessingResults, string>) => valueGetter: (params: GridValueGetterParams<ProcessingResults, string>) =>
Array.isArray(params.row?.cc) params.row?.cc && Array.isArray(params.row.cc)
? params.row.cc.map((point: CCPoint) => `${point.value.toFixed(2)}@${point.resolution.toFixed(2)}`).join(', ') ? params.row.cc.map((point: CCPoint) => `${point.value.toFixed(2)}@${point.resolution.toFixed(2)}`).join(', ')
: '', : '',
}, },
@ -128,18 +158,39 @@ const RunDetails: React.FC<RunDetailsProps> = ({ run, onHeightChange, basePath,
headerName: 'CC(1/2)', headerName: 'CC(1/2)',
flex: 1, flex: 1,
valueGetter: (params: GridValueGetterParams<ProcessingResults, string>) => valueGetter: (params: GridValueGetterParams<ProcessingResults, string>) =>
Array.isArray(params.row?.cchalf) params.row?.cchalf && Array.isArray(params.row.cchalf)
? params.row.cchalf.map((point: CCPoint) => `${point.value.toFixed(2)}@${point.resolution.toFixed(2)}`).join(', ') ? params.row.cchalf.map((point: CCPoint) => `${point.value.toFixed(2)}@${point.resolution.toFixed(2)}`).join(', ')
: '', : '',
}, },
{ field: 'completeness', headerName: 'Completeness (%)', flex: 1 }, {
{ field: 'multiplicity', headerName: 'Multiplicity', flex: 1 }, field: 'completeness',
headerName: 'Completeness (%)',
flex: 1,
valueGetter: (params: GridValueGetterParams<ProcessingResults, string>) =>
params.row?.completeness
? Array.isArray(params.row.completeness)
? params.row.completeness.map((value: CCPoint) => `${value.value.toFixed(2)}@${value.resolution.toFixed(2)}`).join(', ')
: params.row.completeness.toFixed(2)
: 'N/A',
},
{
field: 'multiplicity',
headerName: 'Multiplicity',
flex: 1,
valueGetter: (params: GridValueGetterParams<ProcessingResults, string>) =>
params.row?.multiplicity
? Array.isArray(params.row.multiplicity)
? params.row.multiplicity.map((value: CCPoint) => `${value.value.toFixed(2)}@${value.resolution.toFixed(2)}`).join(', ')
: params.row.multiplicity.toFixed(2)
: 'N/A',
},
{ field: 'nobs', headerName: 'N obs.', flex: 1 }, { field: 'nobs', headerName: 'N obs.', flex: 1 },
{ field: 'total_refl', headerName: 'Total Reflections', flex: 1 }, { field: 'total_refl', headerName: 'Total Reflections', flex: 1 },
{ field: 'unique_refl', headerName: 'Unique Reflections', flex: 1 }, { field: 'unique_refl', headerName: 'Unique Reflections', flex: 1 },
{ field: 'comments', headerName: 'Comments', flex: 2 }, { field: 'comments', headerName: 'Comments', flex: 2 },
]; ];
const updateHeight = () => { const updateHeight = () => {
if (containerRef.current) { if (containerRef.current) {
const newHeight = containerRef.current.offsetHeight; const newHeight = containerRef.current.offsetHeight;
@ -311,31 +362,71 @@ const RunDetails: React.FC<RunDetailsProps> = ({ run, onHeightChange, basePath,
</div> </div>
{processingResult && processingResult.length > 0 && ( {processingResult && processingResult.length > 0 && (
<div style={{width: 400, marginTop: '16px' }}> <div style={{width: 400, marginTop: '16px'}}>
<Typography variant="h6" gutterBottom>CC and CC(1/2) vs Resolution</Typography> <Typography variant="h6" gutterBottom>Processing Metrics vs Resolution</Typography>
<LineChart <LineChart
xAxis={[ xAxis={[
{ {
data: processingResult[0].cc data: processingResult[0].cc
.map((point) => point.resolution) // Grab the resolution values .map((point) => point.resolution) // Use resolution values for the x-axis
.reverse(), // Reverse the data for resolution .reverse(), // Reverse the resolution values to go from high-res to low-res
label: 'Resolution (Å)', label: 'Resolution (Å)',
reverse: true, // This ensures the visual flip on the chart, low-res to right and high-res to left reverse: true, // Flip visually so low-res is to the right
}, },
]} ]}
series={[ series={[
{ {
data: processingResult[0].cc data: processingResult[0].cc
.map((point) => point.value) .map((point) => point.value) // Map CC values
.reverse(), // Reverse the CC values to match the reversed resolution .reverse(), // Reverse order for visual consistency
label: 'CC', label: 'CC',
}, },
{ {
data: processingResult[0].cchalf data: processingResult[0].cchalf
.map((point) => point.value) .map((point) => point.value) // Map CC(1/2) values
.reverse(), // Reverse the CC(1/2) values to match the reversed resolution .reverse(),
label: 'CC(1/2)', label: 'CC(1/2)',
}, },
{
data: Array.isArray(processingResult[0].rmerge)
? processingResult[0].rmerge
.map((point: CCPoint) => point.value) // Map Rmerge values
.reverse()
: [], // Handle edge case where Rmerge isn't an array
label: 'Rmerge',
},
{
data: Array.isArray(processingResult[0].rmeas)
? processingResult[0].rmeas
.map((point: CCPoint) => point.value) // Map Rmeas values
.reverse()
: [],
label: 'Rmeas',
},
{
data: Array.isArray(processingResult[0].isig)
? processingResult[0].isig
.map((point: CCPoint) => point.value) // Map I/sig(I) values
.reverse()
: [],
label: 'I/sig(I)',
},
{
data: Array.isArray(processingResult[0].completeness)
? processingResult[0].completeness
.map((point: CCPoint) => point.value) // Map Completeness values
.reverse()
: [],
label: 'Completeness (%)',
},
{
data: Array.isArray(processingResult[0].multiplicity)
? processingResult[0].multiplicity
.map((point: CCPoint) => point.value) // Map Multiplicity values
.reverse()
: [],
label: 'Multiplicity',
},
]} ]}
height={300} height={300}
/> />

View File

@ -1,10 +1,16 @@
// Planning.tsx
import React from 'react'; import React from 'react';
import CustomCalendar from '../components/Calendar.tsx'; import CustomCalendar from '../components/Calendar.tsx';
const PlanningView: React.FC = () => { interface PlanningViewProps {
return <CustomCalendar />; onPgroupChange?: (pgroup: string) => void;
//return <div>Welcome to the Planning Page</div>; activePgroup: string;
}
const PlanningView: React.FC<PlanningViewProps> = ({ onPgroupChange, activePgroup }) => {
return <CustomCalendar
activePgroup={activePgroup}
onPgroupChange={onPgroupChange}
/>;
}; };
export default PlanningView; export default PlanningView;

View File

@ -446,21 +446,21 @@
{ {
"metadata": { "metadata": {
"ExecuteTime": { "ExecuteTime": {
"end_time": "2025-05-08T13:31:36.929465Z", "end_time": "2025-05-08T15:17:09.428353Z",
"start_time": "2025-05-08T13:31:36.925054Z" "start_time": "2025-05-08T15:17:09.424769Z"
} }
}, },
"cell_type": "code", "cell_type": "code",
"source": "sample_id = 44", "source": "sample_id = 106",
"id": "54d4d46ca558e7b9", "id": "54d4d46ca558e7b9",
"outputs": [], "outputs": [],
"execution_count": 28 "execution_count": 36
}, },
{ {
"metadata": { "metadata": {
"ExecuteTime": { "ExecuteTime": {
"end_time": "2025-05-08T13:31:40.023546Z", "end_time": "2025-05-08T15:17:16.752451Z",
"start_time": "2025-05-08T13:31:39.978510Z" "start_time": "2025-05-08T15:17:16.719047Z"
} }
}, },
"cell_type": "code", "cell_type": "code",
@ -478,7 +478,7 @@
" sample_event_create = SampleEventCreate(\n", " sample_event_create = SampleEventCreate(\n",
" sample_id=sample_id,\n", " sample_id=sample_id,\n",
" #event_type=\"Centering\" # Valid event type\n", " #event_type=\"Centering\" # Valid event type\n",
" event_type=\"Collecting\" # Valid event type\n", " event_type=\"Mounting\" # Valid event type\n",
" )\n", " )\n",
"\n", "\n",
" # Debug the payload before sending\n", " # Debug the payload before sending\n",
@ -512,7 +512,7 @@
"DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): localhost:8000\n", "DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): localhost:8000\n",
"/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/urllib3/connectionpool.py:1103: InsecureRequestWarning: Unverified HTTPS request is being made to host 'localhost'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings\n", "/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/urllib3/connectionpool.py:1103: InsecureRequestWarning: Unverified HTTPS request is being made to host 'localhost'. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings\n",
" warnings.warn(\n", " warnings.warn(\n",
"DEBUG:urllib3.connectionpool:https://localhost:8000 \"POST /samples/samples/44/events HTTP/1.1\" 200 884\n" "DEBUG:urllib3.connectionpool:https://localhost:8000 \"POST /samples/samples/106/events HTTP/1.1\" 200 718\n"
] ]
}, },
{ {
@ -520,25 +520,25 @@
"output_type": "stream", "output_type": "stream",
"text": [ "text": [
"Payload being sent to API:\n", "Payload being sent to API:\n",
"{\"event_type\":\"Collecting\"}\n", "{\"event_type\":\"Mounting\"}\n",
"API response:\n", "API response:\n",
"('id', 44)\n", "('id', 106)\n",
"('sample_name', 'Sample044')\n", "('sample_name', 'Sample106')\n",
"('position', 7)\n", "('position', 13)\n",
"('puck_id', 7)\n", "('puck_id', 11)\n",
"('crystalname', None)\n", "('crystalname', None)\n",
"('proteinname', None)\n", "('proteinname', None)\n",
"('positioninpuck', None)\n", "('positioninpuck', None)\n",
"('priority', None)\n", "('priority', None)\n",
"('comments', None)\n", "('comments', None)\n",
"('data_collection_parameters', None)\n", "('data_collection_parameters', None)\n",
"('events', [SampleEventResponse(event_type='Mounting', id=87, sample_id=44, timestamp=datetime.datetime(2025, 5, 7, 10, 16)), SampleEventResponse(event_type='Unmounting', id=88, sample_id=44, timestamp=datetime.datetime(2025, 5, 7, 10, 16, 50)), SampleEventResponse(event_type='Collecting', id=507, sample_id=44, timestamp=datetime.datetime(2025, 5, 8, 13, 31, 40, 6059))])\n", "('events', [SampleEventResponse(event_type='Mounting', id=452, sample_id=106, timestamp=datetime.datetime(2025, 5, 8, 15, 17, 16, 743011))])\n",
"('mount_count', 0)\n", "('mount_count', 0)\n",
"('unmount_count', 0)\n" "('unmount_count', 0)\n"
] ]
} }
], ],
"execution_count": 29 "execution_count": 37
}, },
{ {
"metadata": { "metadata": {