Commit dd462aa2 authored by Markus Scheidgen's avatar Markus Scheidgen
Browse files

Merge branch 'units' into 'v0.9.0'

Added a fixed Pint unit and constant file, added CLI command for creating unit conversion data for the GUI, added new React component for changing the units in the ArchiveBrowser.

See merge request !184
parents 18940633 759f382e
Pipeline #83207 canceled with stages
in 30 seconds
......@@ -70,6 +70,7 @@ RUN cp dist/nomad-lab-*.tar.gz dist/nomad-lab.tar.gz
RUN python -m nomad.cli dev metainfo > gui/src/metainfo.json
RUN python -m nomad.cli dev search-quantities > gui/src/searchQuantities.json
RUN python -m nomad.cli dev toolkit-metadata > gui/src/toolkitMetadata.json
RUN python -m nomad.cli dev units > gui/src/units.js
WORKDIR /install/docs
RUN make html
RUN \
......@@ -90,6 +91,7 @@ COPY --from=build /install/gui/src/metainfo.json /app/src/metainfo.json
COPY --from=build /install/gui/src/searchQuantities.json /app/src/searchQuantities.json
COPY --from=build /install/gui/src/parserMetadata.json /app/src/parserMetadata.json
COPY --from=build /install/gui/src/toolkitMetadata.json /app/src/toolkitMetadata.json
COPY --from=build /install/gui/src/units.js /app/src/units.js
RUN yarn run build
# Build the Encyclopedia GUI in the gui build image
......
import React, { useMemo, useState, useCallback } from 'react'
import PropTypes from 'prop-types'
import { atom, useRecoilState, useRecoilValue } from 'recoil'
......@@ -16,17 +15,36 @@ import { ErrorHandler, ErrorCard } from '../ErrorHandler'
import DOS from '../visualization/DOS'
import { StructureViewer, BrillouinZoneViewer } from '@lauri-codes/materia'
import Markdown from '../Markdown'
import { convert } from '../../utils'
import { UnitSelector } from './UnitSelector'
import { convertSI } from '../../utils'
import { conversionMap } from '../../units'
export const configState = atom({
key: 'config',
default: {
'showMeta': false,
'showCodeSpecific': false,
'showAllDefined': false
'showAllDefined': false,
'energyUnit': 'joule'
}
})
let defaults = {}
for (const dimension in conversionMap) {
const info = conversionMap[dimension]
defaults[dimension] = info.units[0]
}
const override = {
'length': 'angstrom',
'energy': 'electron_volt',
'system': 'custom'
}
defaults = {...defaults, ...override}
export const unitsState = atom({
key: 'units',
default: defaults
})
// Shared instance of the StructureViewer
const viewer = new StructureViewer()
const bzViewer = new BrillouinZoneViewer()
......@@ -50,6 +68,7 @@ ArchiveBrowser.propTypes = ({
function ArchiveConfigForm({searchOptions}) {
const [config, setConfig] = useRecoilState(configState)
const handleConfigChange = event => {
const changes = {[event.target.name]: event.target.checked}
if (changes.showCodeSpecific) {
......@@ -64,21 +83,23 @@ function ArchiveConfigForm({searchOptions}) {
const { url } = useRouteMatch()
return (
<Box marginTop={-6}>
<FormGroup row style={{alignItems: 'flex-end'}}>
<Autocomplete
options={searchOptions}
getOptionLabel={(option) => option.name}
style={{ width: 350 }}
onChange={(_, value) => {
if (value) {
history.push(url + value.path)
}
}}
renderInput={(params) => <TextField {...params} label="search" margin="normal" />}
/>
<Box marginTop={-3} padding={0}>
<FormGroup row style={{alignItems: 'center'}}>
<Box style={{width: 350, height: 60}}>
<Autocomplete
options={searchOptions}
getOptionLabel={(option) => option.name}
style={{ width: 350, marginTop: -20 }}
onChange={(_, value) => {
if (value) {
history.push(url + value.path)
}
}}
renderInput={(params) => <TextField {...params} label="search" margin="normal" />}
/>
</Box>
<Box flexGrow={1} />
<Tooltip title="Enable to also show all code specfic data">
<Tooltip title="Enable to also show all code specific data">
<FormControlLabel
control={
<Checkbox
......@@ -113,6 +134,7 @@ function ArchiveConfigForm({searchOptions}) {
label="definitions"
/>
</Tooltip>
<UnitSelector unitsState={unitsState}></UnitSelector>
</FormGroup>
</Box>
)
......@@ -246,6 +268,7 @@ class QuantityAdaptor extends ArchiveAdaptor {
}
function QuantityItemPreview({value, def}) {
const units = useRecoilState(unitsState)[0]
if (def.type.type_kind === 'reference') {
return <Box component="span" fontStyle="italic">
<Typography component="span">reference ...</Typography>
......@@ -280,9 +303,14 @@ function QuantityItemPreview({value, def}) {
</Typography>
</Box>
} else {
let finalValue = value
let finalUnit = def.unit
if (def.unit) {
[finalValue, finalUnit] = convertSI(value, def.unit, units)
}
return <Box component="span" whiteSpace="nowarp">
<Number component="span" variant="body1" value={value} exp={8} />
{def.unit && <Typography component="span">&nbsp;{def.unit}</Typography>}
<Number component="span" variant="body1" value={finalValue} exp={8} />
{finalUnit && <Typography component="span">&nbsp;{finalUnit}</Typography>}
</Box>
}
}
......@@ -292,10 +320,18 @@ QuantityItemPreview.propTypes = ({
})
function QuantityValue({value, def}) {
// Figure out the units
const units = useRecoilState(unitsState)[0]
let finalValue = value
let finalUnit = def.unit
if (def.unit) {
[finalValue, finalUnit] = convertSI(value, def.unit, units)
}
return <Box
marginTop={2} marginBottom={2} textAlign="center" fontWeight="bold"
>
{def.shape.length > 0 ? <Matrix values={value} shape={def.shape} invert={def.shape.length === 1} /> : <Number value={value} exp={16} variant="body2" />}
{def.shape.length > 0 ? <Matrix values={finalValue} shape={def.shape} invert={def.shape.length === 1} /> : <Number value={finalValue} exp={16} variant="body2" />}
{def.shape.length > 0 &&
<Typography noWrap variant="caption">
({def.shape.map((dimension, index) => <span key={index}>
......@@ -303,7 +339,7 @@ function QuantityValue({value, def}) {
</span>)}&nbsp;)
</Typography>
}
{def.unit && <Typography noWrap>{def.unit}</Typography>}
{def.unit && <Typography noWrap>{finalUnit}</Typography>}
</Box>
}
QuantityValue.propTypes = ({
......@@ -371,14 +407,14 @@ function Overview({section, def}) {
} else if (sectionPath === visualizedSystem.sectionPath && nAtoms === visualizedSystem.nAtoms) {
positionsOnly = true
system = {
positions: convert(section.atom_positions, 'm', 'angstrom')
positions: convertSI(section.atom_positions, 'meter', {length: 'angstrom'}, false)
}
// Completely new system
} else {
system = {
'species': section.atom_species,
'cell': convert(section.lattice_vectors, 'm', 'angstrom'),
'positions': convert(section.atom_positions, 'm', 'angstrom'),
'cell': convertSI(section.lattice_vectors, 'meter', {length: 'angstrom'}, false),
'positions': convertSI(section.atom_positions, 'meter', {length: 'angstrom'}, false),
'pbc': section.configuration_periodic_dimensions
}
}
......@@ -409,6 +445,7 @@ function Overview({section, def}) {
className={style.bands}
data={section}
aspectRatio={1}
unitsState={unitsState}
></BandStructure>
</ErrorHandler>
</Box>
......@@ -453,6 +490,7 @@ function Overview({section, def}) {
className={style.dos}
data={section}
aspectRatio={1 / 2}
unitsState={unitsState}
></DOS>
</ErrorHandler>
}
......
import React, { useCallback, useState } from 'react'
import { useRecoilState } from 'recoil'
import { makeStyles } from '@material-ui/core/styles'
import {
Box,
Button,
Menu,
MenuItem,
FormControl,
InputLabel,
Select,
FormLabel,
FormControlLabel,
RadioGroup,
Radio,
Tooltip
} from '@material-ui/core'
import PropTypes from 'prop-types'
import clsx from 'clsx'
import { conversionMap, unitMap, unitSystems } from '../../units'
/**
* Component that wraps it's children in a container that can be 'floated',
* i.e. displayed on an html element that is positioned relative to the
* viewport and is above all other elements.
*/
export function UnitSelector({className, classes, unitsState, onUnitChange, onSystemChange}) {
// States
const [canSelect, setCanSelect] = useState(true)
const [anchorEl, setAnchorEl] = React.useState(null)
const open = Boolean(anchorEl)
const [units, setUnits] = useRecoilState(unitsState)
// Styles
const useStyles = makeStyles((theme) => {
return {
menuItem: {
width: '10rem'
},
systems: {
margin: theme.spacing(2),
marginTop: theme.spacing(1)
}
}
})
const style = useStyles(classes)
// Callbacks
const openMenu = useCallback((event) => {
setAnchorEl(event.currentTarget)
}, [])
const closeMenu = useCallback(() => {
setAnchorEl(null)
}, [])
const handleSystemChange = useCallback((event) => {
const systemName = event.target.value
let changes = {system: systemName}
if (systemName === 'custom') {
setCanSelect(true)
} else {
setCanSelect(false)
const system = unitSystems[systemName]
changes = {...changes, ...system.units}
}
setUnits({...units, ...changes})
if (onSystemChange) {
onSystemChange(event)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const handleUnitChange = useCallback(event => {
const changes = {[event.target.name]: event.target.value}
if (onUnitChange) {
onUnitChange(event)
}
console.log(changes)
setUnits({...units, ...changes})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// Ordered list of controllable units. It may be smaller than the full list of
// units.
const unitNames = ['energy', 'length', 'force', 'mass', 'time', 'temperature']
const systemNames = ['SI', 'AU']
return (
<Box className={clsx(style.root, className)}>
<Button
aria-controls="customized-menu"
aria-haspopup="true"
variant="outlined"
color="primary"
onClick={openMenu}
>
Select units
</Button>
<Menu
id="select-unit"
anchorEl={anchorEl}
getContentAnchorEl={null}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
keepMounted
open={open}
onClose={closeMenu}
>
<FormControl
component="fieldset"
classes={{root: style.systems}}
>
<FormLabel component="legend">Unit system</FormLabel>
<RadioGroup aria-label="gender" name="gender1" value={units.system} onChange={handleSystemChange}>
<Tooltip title="Custom units">
<FormControlLabel value="custom" control={<Radio />} label="Custom" />
</Tooltip>
{systemNames.map((systemName) => {
const system = unitSystems[systemName]
return <Tooltip key={systemName} title={system.description}>
<FormControlLabel value={systemName} control={<Radio />} label={system.label} />
</Tooltip>
})}
</RadioGroup>
</FormControl>
{unitNames.map((dimension) => {
const unitList = conversionMap[dimension].units
return <MenuItem
key={dimension}
>
<FormControl disabled={!canSelect}>
<InputLabel id="demo-simple-select-label">{dimension}</InputLabel>
<Select
classes={{root: style.menuItem}}
labelId="demo-simple-select-label"
id="demo-simple-select"
name={dimension}
value={units[dimension]}
onChange={handleUnitChange}
>
{unitList.map((unit) => {
const unitLabel = unitMap[unit].label
return <MenuItem key={unit} value={unit}>{unitLabel}</MenuItem>
})}
</Select>
</FormControl>
</MenuItem>
})}
</Menu>
</Box>
)
}
UnitSelector.propTypes = {
/**
* CSS class for the root element.
*/
className: PropTypes.string,
/**
* CSS classes for this component.
*/
classes: PropTypes.object,
/**
* Recoil atom containing the unit configuration that this component will
* attach to.
*/
unitsState: PropTypes.object,
/**
* Callback for unit selection.
*/
onUnitChange: PropTypes.func,
/**
* Callback for unit system selection.
*/
onSystemChange: PropTypes.func
}
UnitSelector.defaultProps = {
float: false
}
import React, {useState, useEffect, useMemo} from 'react'
import { useRecoilValue } from 'recoil'
import PropTypes from 'prop-types'
import { makeStyles, useTheme } from '@material-ui/core/styles'
import clsx from 'clsx'
......@@ -6,11 +7,12 @@ import {
Box
} from '@material-ui/core'
import Plot from '../visualization/Plot'
import { convert, distance, mergeObjects } from '../../utils'
import { convertSI, distance, mergeObjects } from '../../utils'
export default function BandStructure({data, layout, aspectRatio, className, classes, onRelayout, onAfterPlot, onRedraw, onRelayouting}) {
export default function BandStructure({data, layout, aspectRatio, className, classes, onRelayout, onAfterPlot, onRedraw, onRelayouting, unitsState}) {
const [finalData, setFinalData] = useState(undefined)
const [pathSegments, setPathSegments] = useState(undefined)
const units = useRecoilValue(unitsState)
// Styles
const useStyles = makeStyles(
......@@ -89,7 +91,7 @@ export default function BandStructure({data, layout, aspectRatio, className, cla
// Create plot data entry for each band
for (let band of bands) {
band = convert(band, 'joule', 'eV')
band = convertSI(band, 'joule', units, false)
plotData.push(
{
x: path,
......@@ -122,7 +124,7 @@ export default function BandStructure({data, layout, aspectRatio, className, cla
// Create plot data entry for each band
for (let band of bands) {
band = convert(band, 'joule', 'eV')
band = convertSI(band, 'joule', units, false)
plotData.push(
{
x: path,
......@@ -138,7 +140,7 @@ export default function BandStructure({data, layout, aspectRatio, className, cla
}
setFinalData(plotData)
}, [data, theme.palette.primary.main, theme.palette.secondary.main])
}, [data, theme.palette.primary.main, theme.palette.secondary.main, units])
// Merge custom layout with default layout
const tmpLayout = useMemo(() => {
......@@ -241,5 +243,6 @@ BandStructure.propTypes = {
onAfterPlot: PropTypes.func,
onRedraw: PropTypes.func,
onRelayout: PropTypes.func,
onRelayouting: PropTypes.func
onRelayouting: PropTypes.func,
unitsState: PropTypes.object // Recoil atom containing the unit configuration
}
import React, {useState, useEffect, useMemo} from 'react'
import { useRecoilValue } from 'recoil'
import PropTypes from 'prop-types'
import { makeStyles, useTheme } from '@material-ui/core/styles'
import clsx from 'clsx'
......@@ -6,22 +7,23 @@ import {
Box
} from '@material-ui/core'
import Plot from '../visualization/Plot'
import { convert, mergeObjects } from '../../utils'
import { convertSI, convertSILabel, mergeObjects } from '../../utils'
export default function DOS({data, layout, aspectRatio, className, classes, onRelayout, onAfterPlot, onRedraw, onRelayouting}) {
export default function DOS({data, layout, aspectRatio, className, classes, onRelayout, onAfterPlot, onRedraw, onRelayouting, unitsState}) {
const [finalData, setFinalData] = useState(undefined)
const units = useRecoilValue(unitsState)
// Merge custom layout with default layout
const tmpLayout = useMemo(() => {
let defaultLayout = {
yaxis: {
title: {
text: 'Energy (eV)'
text: `Energy (${convertSILabel('joule', units)})`
}
}
}
return mergeObjects(layout, defaultLayout)
}, [layout])
}, [layout, units])
// Styles
const useStyles = makeStyles(
......@@ -45,7 +47,7 @@ export default function DOS({data, layout, aspectRatio, className, classes, onRe
const plotData = []
if (data !== undefined) {
let nChannels = data[valueName].length
let energies = convert(data[energyName], 'joule', 'eV')
let energies = convertSI(data[energyName], 'joule', units, false)
if (nChannels === 2) {
plotData.push(
{
......@@ -74,7 +76,7 @@ export default function DOS({data, layout, aspectRatio, className, classes, onRe
)
}
setFinalData(plotData)
}, [data, theme.palette.primary.main, theme.palette.secondary.main])
}, [data, theme.palette.primary.main, theme.palette.secondary.main, units])
// Compute layout that depends on data.
const computedLayout = useMemo(() => {
......@@ -85,12 +87,12 @@ export default function DOS({data, layout, aspectRatio, className, classes, onRe
let defaultLayout = {
xaxis: {
title: {
text: norm ? 'states/eV/m<sup>3</sup>/atom' : 'states/eV/cell'
text: norm ? convertSILabel('states/joule/m^3/atom', units) : convertSILabel('states/joule/cell', units)
}
}
}
return defaultLayout
}, [data])
}, [data, units])
// Merge the given layout and layout computed from data
const finalLayout = useMemo(() => {
......@@ -123,5 +125,6 @@ DOS.propTypes = {
onAfterPlot: PropTypes.func,
onRedraw: PropTypes.func,
onRelayout: PropTypes.func,
onRelayouting: PropTypes.func
onRelayouting: PropTypes.func,
unitsState: PropTypes.object // Recoil atom containing the unit configuration
}
// Generated by NOMAD CLI. Do not edit manually.
export const unitMap = {
second: {
dimension: 'time',
label: 'Second',
abbreviation: 's'
},
atomic_unit_of_time: {
dimension: 'time',
label: 'Atomic unit of time',
abbreviation: 'atomic_unit_of_time'
},
meter: {
dimension: 'length',
label: 'Meter',
abbreviation: 'm'
},
bohr: {
dimension: 'length',
label: 'Bohr',
abbreviation: 'bohr'
},
angstrom: {
dimension: 'length',
label: '\u00c5ngstrom',
abbreviation: '\u00c5'
},
kilogram: {
dimension: 'mass',
label: 'Kilogram',
abbreviation: 'kg'
},
electron_mass: {
dimension: 'mass',
label: 'Electron mass',
abbreviation: 'm\u2091'
},
unified_atomic_mass_unit: {
dimension: 'mass',
label: 'Unified atomic mass unit',
abbreviation: 'u'
},
ampere: {
dimension: 'current',
label: 'Ampere',
abbreviation: 'A'
},
atomic_unit_of_current: {
dimension: 'current',
label: 'Atomic unit of current',
abbreviation: 'atomic_unit_of_current'
},
mole: {
dimension: 'substance',
label: 'Mole',
abbreviation: 'mole'
},
candela: {
dimension: 'luminosity',
label: 'Candela',
abbreviation: 'cd'
},
kelvin: {
dimension: 'temperature',
label: 'Kelvin',
abbreviation: 'K'
},
celsius: {
dimension: 'temperature',
label: 'Celsius',
abbreviation: '\u00b0C'
},
fahrenheit: {
dimension: 'temperature',
label: 'Fahrenheit',
abbreviation: '\u00b0F'
},
atomic_unit_of_temperature: {
dimension: 'temperature',
label: 'Atomic unit of temperature',
abbreviation: 'atomic_unit_of_temperature'
},
newton: {
dimension: 'force',
label: 'Newton',
abbreviation: 'N'
},
atomic_unit_of_force: {
dimension: 'force',