Rugnux live page: MR scores and the placed model right under the timeline, carved difference maps
- Fo-Fc (and the anomalous map) are carved within the same 4.5 A of the atoms as 2Fo-Fc, not only cropped to the model's box + 6 A, and Fo-Fc starts at +-3.5 sigma: uncarved at 3 sigma it filled the frame with noise peaks. A departure from AareDB's viewer, which carves 2Fo-Fc only. - The anomalous map is on by default only where the status says the data carry anomalous signal. - The model section (molecular-replacement scores + viewer) sits directly under the timeline and slides in when the run ends with a model; the stage stays dark until model and maps are in, then fades up while the camera eases in from further out and turned ~35 deg (1.6 s, ends exactly at the reset view). prefers-reduced-motion turns all of it off. - The page uses up to 2400 px instead of the 1300 px column; Processing and Result are 1:2, the summary tiles flow into as many columns as fit, the viewer is clamp(520px, 64vh, 820px) tall. - Score tiles: placement (copies, group, MR time), TFZ (RFZ, LLG-Z, search resolution), R-free (R-work, unrefined), CC(model, data), fit over random orientations, 2Fo-Fc at atoms, and the strongest anomalous peak only where there is anomalous signal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu (cherry picked from commit 3bb891c60acd2c5618bbc461fabcb7513f5df947)
This commit is contained in:
1 parent
128830d15e
commit
e374ea269f
6 files changed
+151
-37
No files matched your search
@@ -105,8 +105,9 @@ function PanelColumns({ columns = 2, children }: { columns?: number; children: R
|
||||
|
||||
// An item either renders a panel inside the app (render) or links out to an
|
||||
// external page in a new tab (href), e.g. the source repository.
|
||||
// wide: the page uses the screen's width instead of the 1300 px reading column
|
||||
type NavItem = { id: string; label: string; icon: ReactNode; disabled?: boolean;
|
||||
href?: string; render?: () => ReactNode };
|
||||
href?: string; render?: () => ReactNode; wide?: boolean };
|
||||
type NavSection = { subheader?: string; items: NavItem[] };
|
||||
|
||||
/** Documentation / API reference shown inside the app area, with a new-tab escape hatch. */
|
||||
@@ -192,7 +193,7 @@ function App() {
|
||||
{ id: 'data-collection', label: 'Data collection', icon: <VideocamIcon/>, render: () => (
|
||||
<DataCollection frame_time_us={s.detector_settings?.frame_time_us ?? 500}/>
|
||||
)},
|
||||
{ id: 'rugnux', label: 'Rugnux live', icon: <AutoGraphIcon/>, render: () => (
|
||||
{ id: 'rugnux', label: 'Rugnux live', icon: <AutoGraphIcon/>, wide: true, render: () => (
|
||||
<RugnuxPage measuring={isMeasuring || s.broker?.state === 'Busy'}/>
|
||||
)},
|
||||
]},
|
||||
@@ -308,7 +309,7 @@ function App() {
|
||||
<Box component="main" sx={{ flexGrow: 1, p: 3, pb: '84px', minWidth: 0 }}>
|
||||
<Toolbar/>
|
||||
<ErrorMessage s={s.broker}/>
|
||||
<Box sx={{ maxWidth: 1300, mx: 'auto', mt: 2 }}>
|
||||
<Box sx={{ maxWidth: activeItem.wide ? 2400 : 1300, mx: 'auto', mt: 2 }}>
|
||||
{activeItem.render?.()}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -7,21 +7,26 @@ import {useTokenRequired} from "../auth";
|
||||
import EmptyState from "./EmptyState";
|
||||
import RugnuxTimeline from "./RugnuxTimeline";
|
||||
import RugnuxSettings from "./RugnuxSettings";
|
||||
import {CutAlert, lastImageTime, ProbeAlert, RunMessage, StateChip, StepTable, VerdictAlert} from "./RugnuxParts";
|
||||
import {CutAlert, lastImageTime, ModelScores, ProbeAlert, RunMessage, StateChip, StepTable, VerdictAlert} from "./RugnuxParts";
|
||||
import BoltIcon from "@mui/icons-material/Bolt";
|
||||
import {lazy, Suspense} from "react";
|
||||
import {keyframes} from "@mui/system";
|
||||
|
||||
// Mol* is large: loaded only when there is a model to show.
|
||||
const RugnuxStructureViewer = lazy(() => import("./RugnuxStructureViewer"));
|
||||
|
||||
// The summary rows rugnux writes; the ones a crystallographer looks at first get a stronger tile, and
|
||||
// space group and cell share a row.
|
||||
// The summary rows rugnux writes; the ones a crystallographer looks at first get a stronger tile.
|
||||
const KEY_ROW = /unit cell|space group|resolution|signal|r-?free|model/i;
|
||||
const PAIRED_ROW = /space group|unit cell/i;
|
||||
|
||||
function Section({title, children}: { title: string, children: React.ReactNode }) {
|
||||
// The model section's entrance under the timeline, once the run has placed a model
|
||||
const slideIn = keyframes`
|
||||
from { opacity: 0; transform: translateY(24px); }
|
||||
to { opacity: 1; transform: none; }
|
||||
`;
|
||||
|
||||
function Section({title, children, sx}: { title: string, children: React.ReactNode, sx?: object }) {
|
||||
return (
|
||||
<Paper sx={{p: 3, height: "100%"}}>
|
||||
<Paper sx={{p: 3, height: "100%", ...sx}}>
|
||||
<Typography sx={{fontSize: "0.8rem", fontWeight: 700, letterSpacing: ".08em", textTransform: "uppercase",
|
||||
color: "primary.dark", mb: 2}}>
|
||||
{title}
|
||||
@@ -86,12 +91,12 @@ function SummaryTiles({s}: { s: rugnux_live_status }) {
|
||||
if (s.summary.length === 0)
|
||||
return null;
|
||||
return (
|
||||
<Box sx={{display: "grid", gridTemplateColumns: {xs: "1fr", sm: "1fr 1fr"}, gap: 1.5, mt: 2}}>
|
||||
<Box sx={{display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(300px, 1fr))", gap: 1.5, mt: 2}}>
|
||||
{s.summary.map((row, i) => {
|
||||
const key = KEY_ROW.test(row.label);
|
||||
const short = row.value.length <= 24;
|
||||
return (
|
||||
<Box key={i} sx={{p: 1.5, borderRadius: 2, gridColumn: PAIRED_ROW.test(row.label) ? "auto" : "1 / -1",
|
||||
<Box key={i} sx={{p: 1.5, borderRadius: 2,
|
||||
bgcolor: key ? indigo[50] : grey[50],
|
||||
border: "1px solid", borderColor: key ? indigo[100] : grey[200]}}>
|
||||
<Typography sx={{fontSize: "0.75rem", fontWeight: 700, letterSpacing: ".06em",
|
||||
@@ -109,6 +114,23 @@ function SummaryTiles({s}: { s: rugnux_live_status }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ModelSection({s}: { s: rugnux_live_status }) {
|
||||
const m = s.model!;
|
||||
const mrSeconds = s.steps.find(step => step.title === "Molecular replacement")?.duration_s;
|
||||
return (
|
||||
<Section title={(m.placement === "MR" ? "Molecular replacement" : "Model") + (m.predicted ? " · predicted model" : "")}
|
||||
sx={{animation: `${slideIn} 700ms ease-out`, "@media (prefers-reduced-motion: reduce)": {animation: "none"}}}>
|
||||
<ModelScores m={m} mrSeconds={mrSeconds} anomalousSignal={s.anomalous_signal}/>
|
||||
{s.placed_model && <Box sx={{mt: 2}}>
|
||||
<Suspense fallback={<Typography color="text.secondary">Loading viewer...</Typography>}>
|
||||
<RugnuxStructureViewer resultKey={`${s.elapsed_s}`} anomalous={s.anomalous_map}
|
||||
anomalousSignal={s.anomalous_signal}/>
|
||||
</Suspense>
|
||||
</Box>}
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
function RugnuxPage({measuring}: { measuring: boolean }) {
|
||||
const {data: settings} = useQuery({...getConfigRugnuxOptions(), refetchInterval: 2000});
|
||||
const enabled = settings?.enable === true;
|
||||
@@ -138,7 +160,9 @@ function RugnuxPage({measuring}: { measuring: boolean }) {
|
||||
<Section title="Timeline">
|
||||
<RugnuxTimeline s={s} deadline_s={settings?.deadline_s ?? 0}/>
|
||||
</Section>
|
||||
<Box sx={{display: "grid", gridTemplateColumns: {xs: "1fr", lg: "1fr 1fr"}, gap: 2}}>
|
||||
{s.model && s.state !== 'Running' && <ModelSection s={s}/>}
|
||||
<Box sx={{display: "grid", gridTemplateColumns: {xs: "1fr", lg: "minmax(400px, 1fr) 2fr"}, gap: 2,
|
||||
alignItems: "start"}}>
|
||||
<Section title="Processing">
|
||||
<CurrentStep s={s}/>
|
||||
<StepTable steps={s.steps}/>
|
||||
@@ -155,12 +179,6 @@ function RugnuxPage({measuring}: { measuring: boolean }) {
|
||||
</Stack>
|
||||
</Section>
|
||||
</Box>
|
||||
{s.placed_model && s.state !== 'Running' &&
|
||||
<Section title="Placed model and electron density">
|
||||
<Suspense fallback={<Typography color="text.secondary">Loading viewer...</Typography>}>
|
||||
<RugnuxStructureViewer resultKey={`${s.elapsed_s}`} anomalous={s.anomalous_map}/>
|
||||
</Suspense>
|
||||
</Section>}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {Alert, AlertTitle, Box, Chip, Paper, Table, TableBody, TableCell, TableContainer, TableRow, Typography} from "@mui/material";
|
||||
import {indigo, lime, deepOrange, grey} from "@mui/material/colors";
|
||||
import {SxProps, Theme} from "@mui/material/styles";
|
||||
import {rugnux_live_status, rugnux_live_step} from "../client";
|
||||
import {rugnux_live_model, rugnux_live_status, rugnux_live_step} from "../client";
|
||||
|
||||
// Pieces of the rugnux live status shared by the dashboard card and the Rugnux page.
|
||||
|
||||
@@ -113,3 +113,58 @@ export function StepTable({steps}: { steps: rugnux_live_step[] }) {
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function ScoreTile({label, value, sub, accent}: { label: string, value: string, sub?: string, accent?: boolean }) {
|
||||
return (
|
||||
<Box sx={{p: 1.5, borderRadius: 2, border: "1px solid", minWidth: 0,
|
||||
bgcolor: accent ? indigo[50] : grey[50], borderColor: accent ? indigo[100] : grey[200]}}>
|
||||
<Typography sx={{fontSize: "0.72rem", fontWeight: 700, letterSpacing: ".06em", textTransform: "uppercase",
|
||||
color: accent ? "primary.main" : "text.secondary", whiteSpace: "nowrap"}}>{label}</Typography>
|
||||
<Typography sx={{fontSize: "1.6rem", lineHeight: 1.2, fontWeight: 700, color: "primary.dark",
|
||||
fontVariantNumeric: "tabular-nums", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis"}}>
|
||||
{value}</Typography>
|
||||
{sub && <Typography sx={{fontSize: "0.8rem", color: "text.secondary"}}>{sub}</Typography>}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const PLACEMENT: Record<string, string> = {MR: "MR", RIGID_BODY: "Rigid body", NOT_PLACED: "Not placed"};
|
||||
|
||||
// What the run made of the model, in the numbers of the report's MODEL VALIDATION section; mrSeconds is how long
|
||||
// the molecular-replacement step took. Nothing is refined, so the R-factors are those of the model as placed.
|
||||
// The strongest anomalous peak is shown only where the data carry anomalous signal: without it, it is noise.
|
||||
export function ModelScores({m, mrSeconds, anomalousSignal}:
|
||||
{ m: rugnux_live_model, mrSeconds?: number, anomalousSignal: boolean }) {
|
||||
const tiles: { label: string, value: string, sub?: string, accent?: boolean }[] = [];
|
||||
let placedSub = m.reason || m.mr_decision;
|
||||
if (m.placement === "MR" && m.copies !== undefined)
|
||||
placedSub = `${m.copies} ${m.copies === 1 ? "copy" : "copies"} in ${m.mr_space_group}`
|
||||
+ (mrSeconds !== undefined ? `, found in ${mrSeconds.toFixed(1)} s` : "");
|
||||
else if (m.placement === "RIGID_BODY")
|
||||
placedSub = m.rigid_body_shift_A !== undefined
|
||||
? `moved ${m.rigid_body_angle_deg?.toFixed(1)}° and ${m.rigid_body_shift_A.toFixed(2)} Å`
|
||||
: "fits where it is";
|
||||
tiles.push({label: "Placement", value: PLACEMENT[m.placement] ?? m.placement, sub: placedSub,
|
||||
accent: m.placement !== "NOT_PLACED"});
|
||||
if (m.tf_z !== undefined)
|
||||
tiles.push({label: "TFZ", value: m.tf_z.toFixed(1),
|
||||
sub: [m.rf_z !== undefined && `RFZ ${m.rf_z.toFixed(1)}`, m.llg_z !== undefined && `LLG-Z ${m.llg_z.toFixed(1)}`,
|
||||
m.mr_d_min !== undefined && `search to ${m.mr_d_min.toFixed(1)} Å`].filter(Boolean).join(" · ")});
|
||||
if (m.r_free !== undefined)
|
||||
tiles.push({label: "R-free", value: m.r_free.toFixed(3), accent: true,
|
||||
sub: `R-work ${m.r_work?.toFixed(3)} · model as placed, unrefined`});
|
||||
if (m.cc_model !== undefined)
|
||||
tiles.push({label: "CC (model, data)", value: m.cc_model.toFixed(2), sub: "intensities, all reflections"});
|
||||
if (m.fit_sigma !== undefined)
|
||||
tiles.push({label: "Fit over random", value: `${m.fit_sigma >= 0 ? "+" : ""}${m.fit_sigma.toFixed(1)} σ`,
|
||||
sub: "R-work against the model in random orientations"});
|
||||
if (m.map_atom_sigma !== undefined)
|
||||
tiles.push({label: "2Fo−Fc at atoms", value: `${m.map_atom_sigma.toFixed(1)} σ`, sub: "mean at the atom centres"});
|
||||
if (anomalousSignal && m.anomalous_site && m.anomalous_site_sigma !== undefined)
|
||||
tiles.push({label: "Anomalous peak", value: `${m.anomalous_site_sigma.toFixed(1)} σ`, sub: m.anomalous_site});
|
||||
return (
|
||||
<Box sx={{display: "grid", gap: 1.5, gridTemplateColumns: "repeat(auto-fit, minmax(190px, 1fr))"}}>
|
||||
{tiles.map(t => <ScoreTile key={t.label} {...t}/>)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -7,11 +7,14 @@ import {applyModelDisplay, clearScene, getStructureAtoms, getStructureBox, Loade
|
||||
from "./structure/loadModel";
|
||||
import {loadMapLayer, MAP_COLORS, MapLayer, setLayerVisible, updateLayerSigma} from "./structure/loadMaps";
|
||||
import {fullCellFromCcp4, prepareMap} from "./structure/densityCrop";
|
||||
import {revealCamera} from "./structure/reveal";
|
||||
|
||||
// The model rugnux live placed, with its 2mFo-DFc, mFo-DFc and (where the merge kept the Bijvoet pairs) anomalous
|
||||
// difference maps, in Mol*. Set up as aaredb's structure viewer (gitea.psi.ch/mx/aaredb, pages/StructureView.tsx
|
||||
// and components/Structure/, the PSI MX group's own code): its plugin, Coot mouse controls, model presets, map
|
||||
// meshes, crop and carve, panel and defaults - on a black stage. Mol* is only loaded with this component
|
||||
// meshes, crop and carve, panel and defaults - on a black stage. Departures for the live page: every map is carved
|
||||
// within the same radius of the atoms (aaredb carves 2Fo-Fc only), Fo-Fc starts at +-3.5 sigma, the anomalous map
|
||||
// is shown at first only where the data carry anomalous signal, and the first model makes an entrance (reveal.ts). Mol* is only loaded with this component
|
||||
// (React.lazy in RugnuxPage), and every Mol* import lives in ./structure.
|
||||
|
||||
type Kind = 'twofofc' | 'fofc' | 'anom';
|
||||
@@ -60,8 +63,14 @@ async function fetchBytes(path: string, signal: AbortSignal): Promise<ArrayBuffe
|
||||
return res.arrayBuffer();
|
||||
}
|
||||
|
||||
// resultKey: changes when a new result is there to load; anomalous: it has an anomalous difference map.
|
||||
function RugnuxStructureViewer({resultKey, anomalous}: { resultKey: string, anomalous: boolean }) {
|
||||
const REVEAL_MS = 1600;
|
||||
const reducedMotion = () => window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
|
||||
const nextFrames = () => new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
|
||||
|
||||
// resultKey: changes when a new result is there to load; anomalous: it has an anomalous difference map;
|
||||
// anomalousSignal: the data carry usable anomalous signal, so that map is worth showing from the start.
|
||||
function RugnuxStructureViewer({resultKey, anomalous, anomalousSignal}:
|
||||
{ resultKey: string, anomalous: boolean, anomalousSignal: boolean }) {
|
||||
const pluginRef = useRef<PluginContext | null>(null);
|
||||
const [pluginReady, setPluginReady] = useState(0);
|
||||
const [error, setError] = useState<string>();
|
||||
@@ -73,10 +82,14 @@ function RugnuxStructureViewer({resultKey, anomalous}: { resultKey: string, anom
|
||||
// First load frames the molecule; later results keep the user's camera and clip (aaredb StructureView.tsx:117-121)
|
||||
const sceneLoadedRef = useRef(false);
|
||||
const [hasMap, setHasMap] = useState<Record<Kind, boolean>>({twofofc: false, fofc: false, anom: false});
|
||||
// The stage stays dark until the first model and its maps are in, then fades up as the camera eases in.
|
||||
const [revealed, setRevealed] = useState(false);
|
||||
|
||||
// aaredb factory defaults (state/viewerDefaults.ts:16-23); anomalous as a difference map, +4 sigma
|
||||
const [show, setShow] = useState<Record<Kind, boolean>>({twofofc: true, fofc: true, anom: true});
|
||||
const [sigma, setSigma] = useState<Record<Kind, number>>({twofofc: 1.0, fofc: 3.0, anom: 4.0});
|
||||
// aaredb factory defaults (state/viewerDefaults.ts:16-23), but Fo-Fc at +-3.5 sigma rather than 3: carved or not,
|
||||
// a difference map at 3 sigma over a whole molecule shows dozens of noise peaks; anomalous as a difference map,
|
||||
// +4 sigma
|
||||
const [show, setShow] = useState<Record<Kind, boolean>>({twofofc: true, fofc: true, anom: anomalousSignal});
|
||||
const [sigma, setSigma] = useState<Record<Kind, number>>({twofofc: 1.0, fofc: 3.5, anom: 4.0});
|
||||
const [reprMode, setReprMode] = useState<ReprMode>('cartoon');
|
||||
const [showWaters, setShowWaters] = useState(false);
|
||||
const displayRef = useRef({show, sigma, reprMode, showWaters});
|
||||
@@ -87,8 +100,8 @@ function RugnuxStructureViewer({resultKey, anomalous}: { resultKey: string, anom
|
||||
setPluginReady(n => n + 1);
|
||||
}, []);
|
||||
|
||||
// The model, then its maps (aaredb StructureView.tsx maybeLoad / computeAndAttachMaps, 212-295): 2Fo-Fc cropped
|
||||
// to the model's box + 6 A and carved 4.5 A around the atoms; the difference maps cropped only.
|
||||
// The model, then its maps (aaredb StructureView.tsx maybeLoad / computeAndAttachMaps, 212-295): each map cropped
|
||||
// to the model's box + 6 A and carved 4.5 A around the atoms.
|
||||
useEffect(() => {
|
||||
const plugin = pluginRef.current;
|
||||
if (!plugin)
|
||||
@@ -113,7 +126,8 @@ function RugnuxStructureViewer({resultKey, anomalous}: { resultKey: string, anom
|
||||
setError("No placed model");
|
||||
return;
|
||||
}
|
||||
const loaded = await loadModel(plugin, cif, 'model', {resetCamera: !sceneLoadedRef.current, format: 'mmcif'});
|
||||
const firstScene = !sceneLoadedRef.current;
|
||||
const loaded = await loadModel(plugin, cif, 'model', {resetCamera: firstScene, format: 'mmcif'});
|
||||
sceneLoadedRef.current = true;
|
||||
modelRefsRef.current = loaded.refs;
|
||||
const d = displayRef.current;
|
||||
@@ -128,14 +142,18 @@ function RugnuxStructureViewer({resultKey, anomalous}: { resultKey: string, anom
|
||||
for (const [kind, name, bytes] of maps) {
|
||||
if (!bytes || abort.signal.aborted)
|
||||
continue;
|
||||
const computed = prepareMap(name, fullCellFromCcp4(bytes), crop,
|
||||
kind === 'twofofc' && atoms.length ? atoms : null);
|
||||
const computed = prepareMap(name, fullCellFromCcp4(bytes), crop, atoms.length ? atoms : null);
|
||||
const layer = await loadMapLayer(plugin, computed, name, d.sigma[kind]);
|
||||
layersRef.current[kind] = layer;
|
||||
if (!displayRef.current.show[kind])
|
||||
setLayerVisible(plugin, layer, false);
|
||||
setHasMap(h => ({...h, [kind]: true}));
|
||||
}
|
||||
if (firstScene && !abort.signal.aborted) {
|
||||
await nextFrames(); // the camera reset loadModel asked for is applied on a draw
|
||||
if (!reducedMotion())
|
||||
revealCamera(plugin, REVEAL_MS);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!abort.signal.aborted)
|
||||
setError(e instanceof Error ? e.message : String(e));
|
||||
@@ -143,6 +161,7 @@ function RugnuxStructureViewer({resultKey, anomalous}: { resultKey: string, anom
|
||||
if (!abort.signal.aborted) {
|
||||
setModelLoading(false);
|
||||
setMapsLoading(false);
|
||||
setRevealed(true);
|
||||
}
|
||||
}
|
||||
})();
|
||||
@@ -182,11 +201,15 @@ function RugnuxStructureViewer({resultKey, anomalous}: { resultKey: string, anom
|
||||
return (
|
||||
<Box sx={{display: 'grid', gridTemplateColumns: {xs: '1fr', md: '1fr 300px'}, border: '1px solid',
|
||||
borderColor: 'divider', borderRadius: 1, overflow: 'hidden'}}>
|
||||
<Box sx={{position: 'relative', bgcolor: '#000', height: {xs: '55vh', md: 560}}}>
|
||||
<MolstarCanvas onReady={onPluginReady}/>
|
||||
{modelLoading && !error && <Box sx={{position: 'absolute', inset: 0, display: 'flex', alignItems: 'center',
|
||||
<Box sx={{position: 'relative', bgcolor: '#000', height: {xs: '55vh', md: 'clamp(520px, 64vh, 820px)'}}}>
|
||||
<Box sx={{position: 'absolute', inset: 0, opacity: revealed ? 1 : 0,
|
||||
transition: 'opacity 900ms ease-out', '@media (prefers-reduced-motion: reduce)': {transition: 'none'}}}>
|
||||
<MolstarCanvas onReady={onPluginReady}/>
|
||||
</Box>
|
||||
{(modelLoading || (mapsLoading && !revealed)) && !error && <Box sx={{position: 'absolute', inset: 0, display: 'flex', alignItems: 'center',
|
||||
justifyContent: 'center', pointerEvents: 'none'}}>
|
||||
<Typography sx={{fontSize: '0.875rem', color: '#8aa0b5'}}>loading model…</Typography>
|
||||
<Typography sx={{fontSize: '0.875rem', color: '#8aa0b5'}}>
|
||||
{modelLoading ? 'loading model…' : 'computing density maps…'}</Typography>
|
||||
</Box>}
|
||||
</Box>
|
||||
<Box sx={{borderLeft: {md: '1px solid'}, borderColor: {md: 'divider'}, p: 1.75}}>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// The density preparation of aaredb's density worker (gitea.psi.ch/mx/aaredb, the PSI MX group's own code,
|
||||
// frontend/public/structure-worker/density-worker.js at 8ece2c2): crop the periodic map to the model's box plus a
|
||||
// margin, carve the 2Fo-Fc map around the atoms, encode as CCP4 with the full cell's mean and RMS. aaredb computes
|
||||
// margin, carve it around the atoms, encode as CCP4 with the full cell's mean and RMS. aaredb computes
|
||||
// the full-cell map from an MTZ with gemmi wasm; here the broker serves it (/rugnux/map.ccp4), so it is first read
|
||||
// back into aaredb's full-cell z-fastest grid (fullCellFromCcp4).
|
||||
|
||||
@@ -226,8 +226,9 @@ function carveBlock(block: Float32Array, start: number[], dims: number[], n: num
|
||||
}
|
||||
}
|
||||
|
||||
/* One map as the worker's onmessage loop makes it: cropped to `crop`, carved around `carveAtoms` (2Fo-Fc only -
|
||||
* never a difference map: unmodelled blobs sit away from every atom), with the full cell's mean and RMS. */
|
||||
/* One map as the worker's onmessage loop makes it: cropped to `crop`, carved around `carveAtoms`, with the full
|
||||
* cell's mean and RMS. aaredb carves the 2Fo-Fc map only; the live page carves the difference maps as well, within
|
||||
* the same radius, so what it shows is the density about the model - unmodelled blobs further away are left out. */
|
||||
export function prepareMap(name: string, full: FullCellMap, crop: CropBox | null, carveAtoms: Float32Array | null,
|
||||
carveRadiusA = 4.5): ComputedMap {
|
||||
const [nx, ny, nz] = full.n;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { PluginContext } from 'molstar/lib/mol-plugin/context';
|
||||
import { Quat, Vec3 } from 'molstar/lib/mol-math/linear-algebra';
|
||||
|
||||
// The entrance of a freshly placed model: the camera starts further out and turned a little about the vertical,
|
||||
// then eases in to the view a plain reset frames. Only the camera moves, and it ends exactly where the reset put
|
||||
// it, so nothing after the reveal differs from a viewer without one.
|
||||
export function revealCamera(plugin: PluginContext, durationMs: number): void {
|
||||
const camera = plugin.canvas3d?.camera;
|
||||
if (!camera) return;
|
||||
const end = camera.getSnapshot();
|
||||
const dir = Vec3.sub(Vec3(), end.position, end.target);
|
||||
Vec3.transformQuat(dir, dir, Quat.setAxisAngle(Quat(), end.up, -0.6)); // about 35 degrees
|
||||
Vec3.scale(dir, dir, 2.2);
|
||||
camera.setState({ ...end, position: Vec3.add(Vec3(), end.target, dir) }, 0);
|
||||
camera.setState(end, durationMs, { easing: 'cubic-out' });
|
||||
}
|
||||
Reference in new issue
Block a user