Add beamtime relationships and enhance sample handling

This commit adds relationships to link Pucks and Samples to Beamtime in the models, enabling better data association. Includes changes to assign beamtime IDs during data generation and updates in API response models for improved data loading. Removed redundant code in testfunctions.ipynb to clean up the notebook.
This commit is contained in:
GotthardG 2025-05-06 11:28:36 +02:00
parent 102a11eed7
commit 4328b84795
10 changed files with 222 additions and 28 deletions

View File

@ -407,9 +407,9 @@ beamtimes = [
),
Beamtime(
id=2,
pgroups="p20002",
pgroups="p20003",
shift="afternoon",
beamtime_name="p20001-test",
beamtime_name="p20003-test",
beamline="X06DA",
start_date=datetime.strptime("07.05.2025", "%d.%m.%Y").date(),
end_date=datetime.strptime("08.05.2025", "%d.%m.%Y").date(),
@ -677,8 +677,15 @@ pucks = [
# Define samples
samples = []
sample_id_counter = 1
# Assign a beamtime to each dewar
dewar_to_beamtime = {
dewar.id: random.choice([1, 2]) for dewar in dewars # Or use actual beamtime ids
}
for puck in pucks:
dewar_id = puck.dewar_id # Assuming puck has dewar_id
assigned_beamtime = dewar_to_beamtime[dewar_id]
positions_with_samples = random.randint(1, 16)
occupied_positions = random.sample(range(1, 17), positions_with_samples)
@ -689,6 +696,7 @@ for puck in pucks:
sample_name=f"Sample{sample_id_counter:03}",
position=pos,
puck_id=puck.id,
beamtime_id=assigned_beamtime, # IMPORTANT: Use the dewar's beamtime
)
samples.append(sample)
sample_id_counter += 1

View File

@ -154,6 +154,10 @@ class Puck(Base):
dewar = relationship("Dewar", back_populates="pucks")
samples = relationship("Sample", back_populates="puck")
events = relationship("PuckEvent", back_populates="puck")
beamtime_id = Column(Integer, ForeignKey("beamtimes.id"), nullable=True)
beamtime = relationship(
"Beamtime", back_populates="pucks", foreign_keys=[beamtime_id]
)
class Sample(Base):
@ -173,6 +177,8 @@ class Sample(Base):
puck = relationship("Puck", back_populates="samples")
events = relationship("SampleEvent", back_populates="sample", lazy="joined")
images = relationship("Image", back_populates="sample", lazy="joined")
beamtime_id = Column(Integer, ForeignKey("beamtimes.id"), nullable=True)
beamtime = relationship("Beamtime", back_populates="samples")
@property
def mount_count(self) -> int:
@ -256,6 +262,8 @@ class Beamtime(Base):
local_contact = relationship("LocalContact")
dewars = relationship("Dewar", back_populates="beamtime")
pucks = relationship("Puck", back_populates="beamtime")
samples = relationship("Sample", back_populates="beamtime")
class Image(Base):

View File

@ -1,9 +1,14 @@
from fastapi import APIRouter, HTTPException, status, Depends
from sqlalchemy.orm import Session
from sqlalchemy.orm import Session, joinedload
from sqlalchemy import or_
from app.models import Beamtime as BeamtimeModel
from app.schemas import Beamtime as BeamtimeSchema, BeamtimeCreate, loginData
from app.schemas import (
Beamtime as BeamtimeSchema,
BeamtimeCreate,
loginData,
BeamtimeResponse,
)
from app.dependencies import get_db
from app.routers.auth import get_current_user
@ -60,7 +65,7 @@ async def create_beamtime(
@beamtime_router.get(
"/my-beamtimes",
response_model=list[BeamtimeSchema],
response_model=list[BeamtimeResponse],
)
async def get_my_beamtimes(
db: Session = Depends(get_db),
@ -68,5 +73,10 @@ async def get_my_beamtimes(
):
user_pgroups = current_user.pgroups
filters = [BeamtimeModel.pgroups.like(f"%{pgroup}%") for pgroup in user_pgroups]
beamtimes = db.query(BeamtimeModel).filter(or_(*filters)).all()
beamtimes = (
db.query(BeamtimeModel)
.options(joinedload(BeamtimeModel.local_contact))
.filter(or_(*filters))
.all()
)
return beamtimes

View File

@ -425,6 +425,7 @@ def create_result(payload: ResultCreate, db: Session = Depends(get_db)):
result_entry = ResultsModel(
sample_id=payload.sample_id,
status=payload.status,
run_id=payload.run_id,
result=payload.result.model_dump(), # Serialize entire result to JSON
)
@ -435,6 +436,7 @@ def create_result(payload: ResultCreate, db: Session = Depends(get_db)):
return ResultResponse(
id=result_entry.id,
status=result_entry.status,
sample_id=result_entry.sample_id,
run_id=result_entry.run_id,
result=payload.result, # return original payload directly

View File

@ -534,6 +534,7 @@ class PuckCreate(BaseModel):
puck_type: str
puck_location_in_dewar: int
samples: List[SampleCreate] = []
beamtime_id: Optional[int] = None
class PuckUpdate(BaseModel):
@ -541,6 +542,7 @@ class PuckUpdate(BaseModel):
puck_type: Optional[str] = None
puck_location_in_dewar: Optional[int] = None
dewar_id: Optional[int] = None
beamtime_id: Optional[int] = None
class Puck(BaseModel):
@ -549,6 +551,7 @@ class Puck(BaseModel):
puck_type: str
puck_location_in_dewar: int
dewar_id: int
beamtime_id: Optional[int] = None
events: List[PuckEvent] = []
samples: List[Sample] = []
@ -800,6 +803,24 @@ class BeamtimeCreate(BaseModel):
local_contact_id: Optional[int]
class BeamtimeResponse(BaseModel):
id: int
pgroups: str
shift: str
beamtime_name: str
beamline: str
start_date: date
end_date: date
status: str
comments: Optional[str] = None
proposal_id: Optional[int]
local_contact_id: Optional[int]
local_contact: Optional[LocalContact]
class Config:
from_attributes = True
class ImageCreate(BaseModel):
pgroup: str
sample_id: int

View File

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

View File

@ -12,6 +12,7 @@ import AddressManager from './pages/AddressManagerView';
import ContactsManager from './pages/ContactsManagerView';
import LoginView from './pages/LoginView';
import ProtectedRoute from './components/ProtectedRoute';
import BeamtimeOverview from './components/BeamtimeOverview';
const App: React.FC = () => {
const [openAddressManager, setOpenAddressManager] = useState(false);
@ -84,7 +85,12 @@ const App: React.FC = () => {
<Route path="/" element={<ProtectedRoute element={<HomePage />} />} />
<Route path="/shipments" element={<ProtectedRoute element={<ShipmentView pgroups={pgroups} activePgroup={activePgroup} />} />} />
<Route path="/planning" element={<ProtectedRoute element={<PlanningView />} />} />
<Route path="/results" element={<ProtectedRoute element={<ResultsView pgroups={pgroups} activePgroup={activePgroup} />} />} />
<Route path="/results/:beamtimeId" element={<ProtectedRoute element={<ResultsView pgroups={pgroups} activePgroup={activePgroup} />} />} />
<Route path="/beamtime-overview" element={<ProtectedRoute element={<BeamtimeOverview activePgroup={activePgroup} />} />} />
<Route path="/results" element={<ProtectedRoute element={<BeamtimeOverview activePgroup={activePgroup} />} />}/>
{/* Optionally, add a 404 fallback route */}
<Route path="*" element={<div>Page not found</div>} />
</Routes>
<Modal open={openAddressManager} onClose={handleCloseAddressManager} title="Address Management">
<AddressManager pgroups={pgroups} activePgroup={activePgroup} />

View File

@ -0,0 +1,138 @@
import React, { useEffect, useState } from 'react';
import { DataGridPremium, GridColDef } from '@mui/x-data-grid-premium';
import { useNavigate } from 'react-router-dom'; // For navigation
import { BeamtimesService } from '../../openapi';
import { Chip, Typography } from '@mui/material';
interface BeamtimeRecord {
id: number;
start_date: string;
end_date: string;
shift: string;
beamline: string;
local_contact: string;
pgroups: string;
}
interface BeamtimeOverviewProps {
activePgroup: string;
}
const BeamtimeOverview: React.FC<BeamtimeOverviewProps> = ({ activePgroup }) => {
const [rows, setRows] = useState<BeamtimeRecord[]>([]);
const [isLoading, setIsLoading] = useState(false);
// For navigation
const navigate = useNavigate();
const renderPgroupChips = (pgroups: string, activePgroup: string) => {
// Safely handle pgroups as an array
const pgroupsArray = pgroups.split(",").map((pgroup: string) => pgroup.trim());
if (!pgroupsArray.length) {
return <Typography variant="body2">No associated pgroups</Typography>;
}
return pgroupsArray.map((pgroup: string) => (
<Chip
key={pgroup}
label={pgroup}
color={pgroup === activePgroup ? "primary" : "default"} // Highlight active pgroups
sx={{
margin: 0.5,
backgroundColor: pgroup === activePgroup ? '#19d238' : '#b0b0b0',
color: pgroup === 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,
}}
/>
));
};
// Fetch beamtime records from the backend
const fetchBeamtimeRecords = async () => {
try {
setIsLoading(true);
const records = await BeamtimesService.getMyBeamtimesProtectedBeamtimesMyBeamtimesGet(activePgroup);
const mappedRecords: BeamtimeRecord[] = records.map((record: any) => ({
id: record.id,
start_date: record.start_date || 'N/A',
end_date: record.end_date || 'N/A',
shift: record.shift || 'N/A',
beamline: record.beamline || 'N/A',
local_contact: `${record.local_contact.firstname || "N/A"} ${record.local_contact.lastname || "N/A"}`,
pgroups: record.pgroups || '',
}));
setRows(mappedRecords);
} catch (error) {
console.error('Failed to fetch beamtime records:', error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchBeamtimeRecords();
}, [activePgroup]);
// Define table columns, including the "View Results" button
const columns: GridColDef<BeamtimeRecord>[] = [
{ field: 'start_date', headerName: 'Start Date', flex: 1 },
{ field: 'end_date', headerName: 'End Date', flex: 1 },
{ field: 'shift', headerName: "Shift", flex: 1 },
{ field: 'beamline', headerName: 'Beamline', flex: 1 },
{ field: 'local_contact', headerName: 'Local Contact', flex: 1 },
{
field: 'pgroups',
headerName: 'Pgroups',
flex: 2, // Slightly wider column for chips
renderCell: (params) => renderPgroupChips(params.row.pgroups, activePgroup),
},
{
field: 'viewResults',
headerName: 'Actions',
flex: 1,
renderCell: (params) => (
<button
onClick={() => handleViewResults(params.row.id)}
style={{
padding: '6px 12px',
backgroundColor: '#1976d2',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
View Results
</button>
),
},
];
// Navigate to the ResultsView page for the selected beamtime
const handleViewResults = (beamtimeId: number) => {
navigate(`/results/${beamtimeId}`); // Pass the beamtimeId in the URL
};
return (
<div style={{ height: 400, width: '100%' }}>
<h2>Beamtime Overview</h2>
<DataGridPremium
rows={rows}
columns={columns}
loading={isLoading}
disableRowSelectionOnClick
/>
</div>
);
};
export default BeamtimeOverview;

View File

@ -1,24 +1,24 @@
// components/ResultView.tsx
import React from 'react';
import { useParams } from 'react-router-dom';
import SampleTracker from '../components/SampleTracker';
import ResultGrid from '../components/ResultGrid';
interface ResultsViewProps {
activePgroup: string;
}
interface ResultsViewProps {}
const ResultsView: React.FC<ResultsViewProps> = ({activePgroup
}) => {
const ResultsView: React.FC<ResultsViewProps> = () => {
// Get the selected beamtime ID from the URL
const { beamtimeId } = useParams();
return (
<div>
<h1>Results Page</h1>
<SampleTracker activePgroup={activePgroup}/>
<ResultGrid activePgroup={activePgroup} />
</div>
<h2>Results for Beamtime ID: {beamtimeId}</h2>
{/* Use the beamtimeId to filter or query specific results */}
<SampleTracker activePgroup={`beamtime_${beamtimeId}`} />
<ResultGrid activePgroup={`beamtime_${beamtimeId}`} />
</div>
);
};
export default ResultsView;
export default ResultsView;

File diff suppressed because one or more lines are too long