Commit 78e43ee2 authored by Markus Scheidgen's avatar Markus Scheidgen
Browse files

Merge branch 'brillouin-zone' into 'v0.9.0'

Added component for viewing the Brillouin Zone, added a uniform way of dealing with exceptions through React error boundaries (ErrorHandler.js), fixed small issues with plots and structureviewer.

See merge request !180
parents f7eb9903 853468ba
Pipeline #82797 passed with stages
in 28 minutes and 10 seconds
......@@ -4,7 +4,7 @@
"commit": "e98694e",
"private": true,
"dependencies": {
"@lauri-codes/materia": "0.0.5",
"@lauri-codes/materia": "0.0.6",
"@material-ui/core": "^4.0.0",
"@material-ui/icons": "^4.0.0",
"@material-ui/lab": "^4.0.0-alpha.49",
......
import React from 'react'
import clsx from 'clsx'
import { makeStyles } from '@material-ui/core/styles'
import PropTypes from 'prop-types'
import {
Box,
Card,
CardContent,
Typography
} from '@material-ui/core'
import {
Error
} from '@material-ui/icons'
export class ErrorHandler extends React.Component {
state = {
hasError: false
}
static getDerivedStateFromError(error) {
return {
hasError: true,
error: error
}
}
componentDidCatch(error, errorInfo) {
console.log(error, errorInfo)
}
render() {
if (this.state.hasError) {
let msg = this.props.errorHandler ? this.props.errorHandler(this.state.error) : this.props.message
return <ErrorCard
message={msg}
className={this.props.className}
classes={this.props.classes}
></ErrorCard>
}
return this.props.children
}
}
export function ErrorCard({message, className, classes}) {
const useStyles = makeStyles((theme) => {
return {
root: {
},
content: {
paddingBottom: '16px'
},
'content:last-child': {
paddingBottom: '16px !important'
},
title: {
marginBottom: 0
},
pos: {
marginBottom: 12
},
container: {
display: 'flex'
},
errorIcon: {
marginRight: theme.spacing(1)
}
}
})
const style = useStyles(classes)
return <Card className={clsx(style.root, className)}>
<CardContent className={[style.content, style['content:last-child']].join(' ')}>
<Box className={style.container}>
<Error className={style.errorIcon}/>
<Typography className={style.title} color="textSecondary" gutterBottom>
{message}
</Typography>
</Box>
</CardContent>
</Card>
}
ErrorCard.propTypes = ({
message: PropTypes.string,
classes: PropTypes.object,
className: PropTypes.string
})
ErrorHandler.propTypes = ({
children: PropTypes.object,
message: PropTypes.string, // Fixed error message. Provide either this or errorHandler
errorHandler: PropTypes.func, // Function that is called once an error is caught. It recveives the error object as argument and should return an error message as string.
classes: PropTypes.object,
className: PropTypes.string
})
import React, { useMemo } from 'react'
import React, { useMemo, useState, useCallback } from 'react'
import PropTypes from 'prop-types'
import { atom, useRecoilState, useRecoilValue } from 'recoil'
import { Box, FormGroup, FormControlLabel, Checkbox, TextField, Typography, makeStyles, Tooltip } from '@material-ui/core'
import { Box, FormGroup, FormControlLabel, Checkbox, TextField, Typography, makeStyles, Tooltip, FormControl, RadioGroup, Radio } from '@material-ui/core'
import { useRouteMatch, useHistory } from 'react-router-dom'
import Autocomplete from '@material-ui/lab/Autocomplete'
import Browser, { Item, Content, Compartment, List, Adaptor } from './Browser'
......@@ -10,9 +10,11 @@ import { resolveRef, rootSections } from './metainfo'
import { Title, metainfoAdaptorFactory, DefinitionLabel } from './MetainfoBrowser'
import { Matrix, Number } from './visualizations'
import Structure from '../visualization/Structure'
import BrillouinZone from '../visualization/BrillouinZone'
import BandStructure from '../visualization/BandStructure'
import { ErrorHandler, ErrorCard } from '../ErrorHandler'
import DOS from '../visualization/DOS'
import { StructureViewer } from '@lauri-codes/materia'
import { StructureViewer, BrillouinZoneViewer } from '@lauri-codes/materia'
import Markdown from '../Markdown'
import { convert } from '../../utils'
......@@ -27,6 +29,7 @@ export const configState = atom({
// Shared instance of the StructureViewer
const viewer = new StructureViewer()
const bzViewer = new BrillouinZoneViewer()
// Contains details about the currently visualized system. Used to detect if a
// reload is needed for the StructureViewer.
......@@ -313,6 +316,8 @@ QuantityValue.propTypes = ({
* title.
*/
function Overview({section, def}) {
// States
const [mode, setMode] = useState('bs')
// Styles
const useStyles = makeStyles(
{
......@@ -321,17 +326,23 @@ function Overview({section, def}) {
height: '30rem'
},
dos: {
width: '20',
height: '20'
width: '20rem',
height: '40rem'
},
bz: {
width: '20',
height: '20'
error: {
},
radio: {
display: 'flex',
justifyContent: 'center'
}
}
)
const style = useStyles()
const toggleMode = useCallback((event) => {
setMode(event.target.value)
}, [setMode])
// Structure visualization for section_system
if (def.name === 'section_system') {
let url = window.location.href
......@@ -345,10 +356,19 @@ function Overview({section, def}) {
let system
let positionsOnly = false
// Do not attempt to perform visualization if size is too big
const nAtoms = section.atom_species.length
if (nAtoms >= 300) {
return <ErrorCard
message='Visualization is disabled due to large system size.'
className={style.error}
>
</ErrorCard>
}
// Loading exact same system, no need to reload visualizer
if (sectionPath === visualizedSystem.sectionPath && index === visualizedSystem.index) {
// Loading same system with different positions
} else if (sectionPath === visualizedSystem.sectionPath) {
} else if (sectionPath === visualizedSystem.sectionPath && nAtoms === visualizedSystem.nAtoms) {
positionsOnly = true
system = {
positions: convert(section.atom_positions, 'm', 'angstrom')
......@@ -364,26 +384,77 @@ function Overview({section, def}) {
}
visualizedSystem.sectionPath = sectionPath
visualizedSystem.index = index
visualizedSystem.nAtoms = nAtoms
return <Structure
viewer={viewer}
system={system}
positionsOnly={positionsOnly}
></Structure>
return <ErrorHandler
message='Could not load structure viewer.'
className={style.error}
>
<Structure
viewer={viewer}
system={system}
positionsOnly={positionsOnly}
></Structure>
</ErrorHandler>
// Band structure plot for section_k_band or section_k_band_normalized
} else if (def.name === 'section_k_band' || def.name === 'section_k_band_normalized') {
return <BandStructure
className={style.bands}
data={section}
aspectRatio={1}
></BandStructure>
} else if (def.name === 'section_k_band') {
return <>
{mode === 'bs'
? <Box>
<ErrorHandler
message="Could not load the band structure."
className={style.error}
>
<BandStructure
className={style.bands}
data={section}
aspectRatio={1}
></BandStructure>
</ErrorHandler>
</Box>
: <>
<ErrorHandler
message="Could not load the Brillouin zone."
className={style.error}
>
<BrillouinZone
viewer={bzViewer}
className={style.bands}
data={section}
aspectRatio={1}
></BrillouinZone>
</ErrorHandler>
</>
}
<FormControl component="fieldset" className={style.radio}>
<RadioGroup row aria-label="position" name="position" defaultValue="bs" onChange={toggleMode} className={style.radio}>
<FormControlLabel
value="bs"
control={<Radio color="primary" />}
label="Band structure"
labelPlacement="end"
/>
<FormControlLabel
value="bz"
control={<Radio color="primary" />}
label="Brillouin zone"
labelPlacement="end"
/>
</RadioGroup>
</FormControl>
</>
// DOS plot for section_dos
} else if (def.name === 'section_dos') {
return <DOS
className={style.dos}
data={section}
aspectRatio={1 / 2}
></DOS>
return <ErrorHandler
message="Could not load the density of states"
className={style.error}
>
<DOS
className={style.dos}
data={section}
aspectRatio={1 / 2}
></DOS>
</ErrorHandler>
}
return null
}
......
......@@ -144,6 +144,7 @@ export default function BandStructure({data, layout, aspectRatio, className, cla
const tmpLayout = useMemo(() => {
let defaultLayout = {
xaxis: {
tickangle: 0,
tickfont: {
size: 14
}
......
import React, { useState, useEffect, useRef, useCallback } from 'react'
import clsx from 'clsx'
import PropTypes from 'prop-types'
import { makeStyles } from '@material-ui/core/styles'
import { makeStyles, useTheme } from '@material-ui/core/styles'
import {
Box,
Checkbox,
Menu,
MenuItem,
IconButton,
Tooltip,
Typography,
FormControlLabel
Typography
} from '@material-ui/core'
import {
MoreVert,
Fullscreen,
FullscreenExit,
CameraAlt,
......@@ -21,18 +16,13 @@ import {
} from '@material-ui/icons'
import { BrillouinZoneViewer } from '@lauri-codes/materia'
import Floatable from './Floatable'
import { scale, distance } from '../../utils'
export default function BrillouinZone({className, classes, options, viewer, system, positionsOnly, sizeLimit, captureName, aspectRatio}) {
export default function BrillouinZone({className, classes, options, viewer, data, captureName, aspectRatio}) {
// States
const [anchorEl, setAnchorEl] = React.useState(null)
const [fullscreen, setFullscreen] = useState(false)
const [showBonds, setShowBonds] = useState(true)
const [showLatticeConstants, setShowLatticeConstants] = useState(true)
const [showCell, setShowCell] = useState(true)
const [error, setError] = useState(null)
// Variables
const open = Boolean(anchorEl)
const refViewer = useRef(null)
const refCanvas = useRef(null)
......@@ -49,6 +39,7 @@ export default function BrillouinZone({className, classes, options, viewer, syst
backgroundColor: 'white'
},
header: {
paddingRight: theme.spacing(1),
display: 'flex',
flexDirection: 'row',
zIndex: 1
......@@ -60,17 +51,7 @@ export default function BrillouinZone({className, classes, options, viewer, syst
flex: 1,
zIndex: 0,
minHeight: 0, // added min-height: 0 to allow the item to shrink to fit inside the container.
marginBottom: theme.spacing(2),
display: error === null ? 'block' : 'none'
},
errorContainer: {
flex: 1,
zIndex: 0,
minHeight: 0, // added min-height: 0 to allow the item to shrink to fit inside the container.
marginBottom: theme.spacing(2),
alignItems: 'center',
justifyContent: 'center',
display: error === null ? 'none' : 'flex'
marginBottom: theme.spacing(2)
},
errorMessage: {
flex: '0 0 70%',
......@@ -101,35 +82,52 @@ export default function BrillouinZone({className, classes, options, viewer, syst
// Run only on first render to initialize the viewer. See the viewer
// documentation for details on the meaning of different options:
// https://lauri-codes.github.io/materia/viewers/structureviewer
// https://lauri-codes.github.io/materia/viewers/brillouinzoneviewer
const theme = useTheme()
useEffect(() => {
let viewerOptions
if (options === undefined) {
viewerOptions = {
view: {
autoResize: true,
autoFit: true,
fitMargin: 0.5
fitMargin: 0.02
},
bonds: {
enabled: true
layout: {
viewRotation: {
align: {
up: 'a',
segments: 'front'
},
rotations: [
[0, 1, 0, 45],
[1, 0, 0, 25]
]
}
},
latticeConstants: {
size: 0.7,
basis: {
font: 'Titillium Web,sans-serif',
a: {color: '#f44336'},
b: {color: '#4caf50'},
c: {color: '#5c6bc0'}
offset: 0.025,
size: 0.04,
a: { color: '#f44336' },
b: { color: '#4caf50' },
c: { color: '#5c6bc0' }
},
segments: {
color: theme.palette.primary.main
},
controls: {
enableZoom: true,
enablePan: true,
enableRotate: true
faces: {
outline: {
width: 0.002
}
},
renderer: {
backgroundColor: ['#ffffff', 1],
shadows: {
enabled: false
kpoints: {
label: {
color: theme.palette.primary.main,
font: 'Titillium Web,sans-serif',
size: 0.035
},
point: {
color: theme.palette.primary.main,
size: 0.01
}
}
}
......@@ -151,77 +149,49 @@ export default function BrillouinZone({className, classes, options, viewer, syst
// Called only on first render to load the given structure.
useEffect(() => {
if (system === undefined) {
if (data === undefined) {
return
}
if (positionsOnly) {
refViewer.current.setPositions(system.positions)
return
}
let nAtoms = system.species.length
if (nAtoms >= sizeLimit) {
setError('Visualization is disabled due to large system size.')
return
// Format data from section_k_band into the form used by the viewer
const basis = scale(data.reciprocal_cell, 1E-10)
const kpoints = []
const segments = []
const finalData = {
basis: basis,
kpoints: kpoints,
segments: segments
}
// Systems with cell are centered on the cell center and orientation is defined
// by the cell vectors.
let cell = system.cell
if (cell !== undefined) {
refViewer.current.setOptions({layout: {
viewCenter: 'COC',
viewRotation: {
align: {
top: 'c',
right: 'b'
},
rotations: [
[0, 1, 0, 60],
[1, 0, 0, 30]
]
}
}})
// Systems without cell are centered on the center of positions
} else {
refViewer.current.setOptions({layout: {
viewCenter: 'COP',
viewRotation: {
rotations: [
[0, 1, 0, 60],
[1, 0, 0, 30]
]
let previousPoint
let segment = []
for (let seg of data.section_k_band_segment) {
let labels = seg.band_segm_labels
const start = seg.band_k_points[0]
const end = seg.band_k_points.slice(-1)[0]
if (!previousPoint || (previousPoint && distance(start, previousPoint) >= 1e-8)) {
// Push old segment
if (segment.length > 0) {
segments.push(segment)
}
}})
// Start new segment
segment = []
segment.push(start)
kpoints.push([labels[0], start])
}
segment.push(end)
kpoints.push([labels[1], end])
previousPoint = end
}
refViewer.current.load(system)
// Push last segment
segments.push(segment)
// Load data into viewer
refViewer.current.load(finalData)
refViewer.current.fitToCanvas()
refViewer.current.saveReset()
refViewer.current.reset()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// Viewer settings
useEffect(() => {
refViewer.current.setOptions({bonds: {enabled: showBonds}})
}, [showBonds])
useEffect(() => {
refViewer.current.setOptions({latticeConstants: {enabled: showLatticeConstants}})
}, [showLatticeConstants])
useEffect(() => {
refViewer.current.setOptions({cell: {enabled: showCell}})
}, [showCell])
// Memoized callbacks
const openMenu = useCallback((event) => {
setAnchorEl(event.currentTarget)
}, [])
const closeMenu = useCallback(() => {
setAnchorEl(null)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const toggleFullscreen = useCallback(() => {
......@@ -240,79 +210,26 @@ export default function BrillouinZone({className, classes, options, viewer, syst
const content = <Box className={style.container}>
<div className={style.header}>
{fullscreen && <Typography variant="h6">Structure</Typography>}
{fullscreen && <Typography variant="h6">Brillouin zone</Typography>}
<div className={style.spacer}></div>
<Tooltip title="Reset view">
<IconButton className={style.iconButton} onClick={handleReset} disabled={error}>
<IconButton className={style.iconButton} onClick={handleReset}>
<Replay />
</IconButton>
</Tooltip>
<Tooltip
title="Toggle fullscreen">
<IconButton className={style.iconButton} onClick={toggleFullscreen} disabled={error}>
<IconButton className={style.iconButton} onClick={toggleFullscreen}>
{fullscreen ? <FullscreenExit /> : <Fullscreen />}
</IconButton>
</Tooltip>
<Tooltip title="Capture image">
<IconButton className={style.iconButton} onClick={takeScreencapture} disabled={error}>
<IconButton className={style.iconButton} onClick={takeScreencapture}>
<CameraAlt />
</IconButton>
</Tooltip>
<Tooltip title="Options">
<IconButton className={style.iconButton} onClick={openMenu} disabled={error}>
<MoreVert />
</IconButton>
</Tooltip>
<Menu
id='settings-menu'
anchorEl={anchorEl}
getContentAnchorEl={null}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
keepMounted
open={open}
onClose={closeMenu}
>
<Men