From 16dd821aa03268b65bff8924bb741c3989070c03 Mon Sep 17 00:00:00 2001 From: Markus Scheidgen <markus.scheidgen@gmail.com> Date: Tue, 28 Apr 2020 13:12:40 +0200 Subject: [PATCH] Refactored QuantityHistogram. --- .gitignore | 2 +- gui/.gitignore | 1 + gui/src/components/api.js | 30 +- gui/src/components/dft/DFTVisualizations.js | 154 ++--- gui/src/components/ems/EMSVisualizations.js | 12 +- .../components/search/QuantityHistogram.js | 356 +++++----- gui/src/components/search/Search.js | 4 +- gui/src/components/search/SearchContext.js | 10 +- gui/src/config.js | 2 +- gui/src/searchQuantities.json | 612 ++++++++++++++++++ nomad/datamodel/dft.py | 26 +- nomad/gitinfo.py | 2 +- nomad/metainfo/search_extension.py | 18 +- nomad/parsing/__init__.py | 9 +- nomad/search.py | 19 +- 15 files changed, 917 insertions(+), 340 deletions(-) create mode 100644 gui/src/searchQuantities.json diff --git a/.gitignore b/.gitignore index 02bd26eb14..588c18b4e3 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,4 @@ nomad.yaml ./gunicorn.conf build/ dist/ -setup.json \ No newline at end of file +setup.json diff --git a/gui/.gitignore b/gui/.gitignore index dfa47e4841..7d229c31c4 100644 --- a/gui/.gitignore +++ b/gui/.gitignore @@ -12,6 +12,7 @@ # generated public/metainfo/ public/meta.json +str/searchQuantities.json # misc .DS_Store diff --git a/gui/src/components/api.js b/gui/src/components/api.js index 403cee39e9..ba726f9a9c 100644 --- a/gui/src/components/api.js +++ b/gui/src/components/api.js @@ -204,8 +204,6 @@ class Api { } constructor(keycloak) { - this.statistics = {} - this._swaggerClient = Swagger(`${apiBase}/swagger.json`) this.keycloak = keycloak @@ -403,7 +401,7 @@ class Api { .finally(this.onFinishLoading) } - async search(search, statisticsToRefresh = []) { + async search(search) { this.onStartLoading() return this.swagger() .then(client => client.apis.repo.search({ @@ -411,32 +409,6 @@ class Api { ...search})) .catch(handleApiError) .then(response => response.body) - .then(response => { - // fill absent statistics values with values from prior searches - // this helps to keep consistent values, e.g. in the metadata search view - if (response.statistics) { - const empty = {} - const refreshList = ['total', 'authors', 'atoms'].concat(statisticsToRefresh) - Object.keys(response.statistics.total.all).forEach(metric => { - empty[metric] = 0 - }) - Object.keys(response.statistics) - .filter(key => !refreshList.includes(key)) - .forEach(key => { - if (!this.statistics[key]) { - this.statistics[key] = new Set() - } - const values = this.statistics[key] - Object.keys(response.statistics[key]).forEach(value => values.add(value)) - values.forEach(value => { - if (!response.statistics[key][value]) { - response.statistics[key][value] = empty - } - }) - }) - } - return response - }) .finally(this.onFinishLoading) } diff --git a/gui/src/components/dft/DFTVisualizations.js b/gui/src/components/dft/DFTVisualizations.js index 2a717ceaff..073f5efec7 100644 --- a/gui/src/components/dft/DFTVisualizations.js +++ b/gui/src/components/dft/DFTVisualizations.js @@ -1,7 +1,7 @@ import React, { useContext, useEffect } from 'react' import PropTypes from 'prop-types' import { Grid } from '@material-ui/core' -import { Quantity } from '../search/QuantityHistogram' +import QuantityHistogram from '../search/QuantityHistogram' import { searchContext } from '../search/SearchContext' export function DFTMethodVisualizations(props) { @@ -28,11 +28,11 @@ export function DFTMethodVisualizations(props) { return ( <Grid container spacing={2}> <Grid item xs={8}> - <Quantity quantity="dft.code_name" title="Code" scale={0.25} metric={metric} sort columns={2} /> + <QuantityHistogram quantity="dft.code_name" title="Code" initialScale={0.25} columns={2} /> </Grid> <Grid item xs={4}> - <Quantity quantity="dft.basis_set" title="Basis set" scale={0.25} metric={metric} sort /> - <Quantity quantity="dft.xc_functional" title="XC functionals" scale={0.5} metric={metric} sort /> + <QuantityHistogram quantity="dft.basis_set" title="Basis set" initialScale={0.25} /> + <QuantityHistogram quantity="dft.xc_functional" title="XC functionals" initialScale={0.5} /> </Grid> </Grid> ) @@ -44,9 +44,8 @@ DFTMethodVisualizations.propTypes = { export function DFTSystemVisualizations(props) { const {info} = props - const {response: {statistics, metric}, setStatisticsToRefresh, setStatistics} = useContext(searchContext) + const {response: {statistics, metric}, setStatistics} = useContext(searchContext) useEffect(() => { - setStatisticsToRefresh('dft.labels_springer_compound_class') setStatistics(['dft.labels_springer_compound_class', 'dft.system', 'dft.crystal_system', 'dft.compound_type']) }, []) @@ -67,14 +66,14 @@ export function DFTSystemVisualizations(props) { return ( <Grid container spacing={2}> <Grid item xs={4}> - <Quantity quantity="dft.compound_type" title="Compound type" scale={1} metric={metric} sort /> + <QuantityHistogram quantity="dft.compound_type" title="Compound type" initialScale={0.25} /> </Grid> <Grid item xs={4}> - <Quantity quantity="dft.system" title="System type" scale={0.25} metric={metric} sort /> - <Quantity quantity="dft.crystal_system" title="Crystal system" scale={1} metric={metric} sort /> + <QuantityHistogram quantity="dft.system" title="System type" initialScale={0.25} /> + <QuantityHistogram quantity="dft.crystal_system" title="Crystal system" /> </Grid> <Grid item xs={4}> - <Quantity quantity="dft.labels_springer_compound_class" title="Springer compound" scale={1} metric={metric} /> + <QuantityHistogram quantity="dft.labels_springer_compound_class" title="Springer compound" /> </Grid> </Grid> ) @@ -84,51 +83,75 @@ DFTSystemVisualizations.propTypes = { info: PropTypes.object } -const searchable_quantities_categories = { - energy_quantities: [ - 'energy_total', - 'energy_total_T0', - 'energy_free', - 'energy_electrostatic', - 'energy_X', - 'energy_XC', - 'energy_sum_eigenvalues' - ], - electronic_quantities: [ - 'dos_values', - 'eigenvalues_values', - 'volumetric_data_values', - 'electronic_kinetic_energy', - 'total_charge', - 'atomic_multipole_values' - ], - forces_quantities: [ - 'atom_forces_free', - 'atom_forces_raw', - 'atom_forces_T0', - 'atom_forces', - 'stress_tensor' - ], - vibrational_quantities: [ - 'thermodynamical_property_heat_capacity_C_v', - 'vibrational_free_energy_at_constant_volume', - 'band_energies' - ], - magnetic_quantities: [ - 'spin_S2' - ], - optical_quantities: [ - 'excitation_energies', - 'oscillator_strengths', - 'transition_dipole_moments' - ] -} +const energy_quantities = [ + 'energy_total', + 'energy_total_T0', + 'energy_free', + 'energy_electrostatic', + 'energy_X', + 'energy_XC', + 'energy_sum_eigenvalues' +] +const electronic_quantities = [ + 'dos_values', + 'eigenvalues_values', + 'volumetric_data_values', + 'electronic_kinetic_energy', + 'total_charge', + 'atomic_multipole_values' +] +const forces_quantities = [ + 'atom_forces_free', + 'atom_forces_raw', + 'atom_forces_T0', + 'atom_forces', + 'stress_tensor' +] +const vibrational_quantities = [ + 'thermodynamical_property_heat_capacity_C_v', + 'vibrational_free_energy_at_constant_volume', + 'band_energies' +] +const magnetic_quantities = [ + 'spin_S2' +] +const optical_quantities = [ + 'excitation_energies', + 'oscillator_strengths', + 'transition_dipole_moments' +] + +const labels = { + 'energy_total': 'Total energy', + 'energy_total_T0': 'Total energy (0K)', + 'energy_free': 'Free energy', + 'energy_electrostatic': 'Electrostatic', + 'energy_X': 'Exchange', + 'energy_XC': 'Exchange-correlation', + 'energy_sum_eigenvalues': 'Band energy', + 'dos_values': 'DOS', + 'eigenvalues_values': 'Eigenvalues', + 'volumetric_data_values': 'Volumetric data', + 'electronic_kinetic_energy': 'Kinetic energy', + 'total_charge': 'Charge', + 'atom_forces_free': 'Free atomic forces', + 'atom_forces_raw': 'Raw atomic forces', + 'atom_forces_T0': 'Atomic forces (0K)', + 'atom_forces': 'Atomic forces', + 'stress_tensor': 'Stress tensor', + 'thermodynamical_property_heat_capacity_C_v': 'Heat capacity', + 'vibrational_free_energy_at_constant_volume': 'Free energy (const=V)', + 'band_energies': 'Band energies', + 'spin_S2': 'Spin momentum operator', + 'excitation_energies': 'Excitation energies', + 'oscillator_strengths': 'Oscillator strengths', + 'transition_dipole_moments': 'Transition dipole moments', + 'atomic_multipole_values': 'Atomic multipole values'} export function DFTPropertyVisualizations(props) { const {info} = props - const {response: {statistics, metric}, setStatisticsToRefresh, setStatistics} = useContext(searchContext) + const {response: {statistics, metric}, setStatistics} = useContext(searchContext) useEffect(() => { - setStatisticsToRefresh('dft.labels_springer_classification') setStatistics([ 'dft.searchable_quantities', 'dft.labels_springer_classification' @@ -149,35 +172,20 @@ export function DFTPropertyVisualizations(props) { statistics.code_name = filteredCodeNames } - const data = (category) => { - const results = {} - const data = statistics['dft.searchable_quantities'] - if (!data) { - return null - } - - searchable_quantities_categories[category].forEach(value => { - if (data[value]) { - results[value] = data[value] - } - }) - return results - } - return ( <Grid container spacing={2}> <Grid item xs={4}> - <Quantity quantity="dft.searchable_quantities" data={data('energy_quantities')} title="Energy" scale={1} metric={metric} sort tooltips /> - <Quantity quantity="dft.searchable_quantities" data={data('electronic_quantities')} title="Electronic" scale={1} metric={metric} sort tooltips /> + <QuantityHistogram quantity="dft.searchable_quantities" values={energy_quantities} valueLabels={labels} title="Energy" initialScale={0.5} tooltips /> + <QuantityHistogram quantity="dft.searchable_quantities" values={electronic_quantities} valueLabels={labels} title="Electronic" initialScale={0.5} tooltips /> </Grid> <Grid item xs={4}> - <Quantity quantity="dft.searchable_quantities" data={data('forces_quantities')} title="Forces" scale={1} metric={metric} sort tooltips /> - <Quantity quantity="dft.searchable_quantities" data={data('vibrational_quantities')} title="Vibrational" scale={1} metric={metric} sort tooltips /> - <Quantity quantity="dft.searchable_quantities" data={data('optical_quantities')} title="Optical" scale={1} metric={metric} sort tooltips /> + <QuantityHistogram quantity="dft.searchable_quantities" values={forces_quantities} valueLabels={labels} title="Forces" initialScale={0.5} tooltips /> + <QuantityHistogram quantity="dft.searchable_quantities" values={vibrational_quantities} valueLabels={labels} title="Vibrational" initialScale={0.5} tooltips /> + <QuantityHistogram quantity="dft.searchable_quantities" values={optical_quantities} valueLabels={labels} title="Optical" initialScale={1} tooltips /> </Grid> <Grid item xs={4}> - <Quantity quantity="dft.labels_springer_classification" title="Springer classification" scale={1} metric={metric} tooltips /> - <Quantity quantity="dft.searchable_quantities" data={data('magnetic_quantities')} title="Magnetic" scale={1} metric={metric} sort tooltips /> + <QuantityHistogram quantity="dft.labels_springer_classification" title="Springer classification" initialScale={1} tooltips /> + <QuantityHistogram quantity="dft.searchable_quantities" values={magnetic_quantities} valueLabels={labels} title="Magnetic" initialScale={1} tooltips /> </Grid> </Grid> ) diff --git a/gui/src/components/ems/EMSVisualizations.js b/gui/src/components/ems/EMSVisualizations.js index 5e1441be48..22202f01ea 100644 --- a/gui/src/components/ems/EMSVisualizations.js +++ b/gui/src/components/ems/EMSVisualizations.js @@ -1,22 +1,22 @@ import React, { useContext, useEffect } from 'react' import { Grid } from '@material-ui/core' -import { Quantity } from '../search/QuantityHistogram' +import QuantityHistogram from '../search/QuantityHistogram' import { searchContext } from '../search/SearchContext' export default function EMSVisualizations(props) { - const {state: {usedMetric}, setStatistics} = useContext(searchContext) + const {setStatistics} = useContext(searchContext) useEffect(() => { setStatistics(['ems.method', 'ems.probing_method', 'ems.sample_microstructure', 'ems.sample_constituents']) }, []) return ( <Grid container spacing={2}> <Grid item xs={6}> - <Quantity quantity="ems.method" title="Method" scale={1} metric={usedMetric} /> - <Quantity quantity="ems.probing_method" title="Probing" scale={1} metric={usedMetric} /> + <QuantityHistogram quantity="ems.method" title="Method" /> + <QuantityHistogram quantity="ems.probing_method" title="Probing" /> </Grid> <Grid item xs={6}> - <Quantity quantity="ems.sample_microstructure" title="Sample structure" scale={1} metric={usedMetric} /> - <Quantity quantity="ems.sample_constituents" title="Sample constituents" scale={1} metric={usedMetric} /> + <QuantityHistogram quantity="ems.sample_microstructure" title="Sample structure" /> + <QuantityHistogram quantity="ems.sample_constituents" title="Sample constituents" /> </Grid> </Grid> ) diff --git a/gui/src/components/search/QuantityHistogram.js b/gui/src/components/search/QuantityHistogram.js index c9fcf4bd7a..380ce3583e 100644 --- a/gui/src/components/search/QuantityHistogram.js +++ b/gui/src/components/search/QuantityHistogram.js @@ -1,10 +1,11 @@ -import React, { useContext } from 'react' +import React, { useRef, useState, useEffect, useContext } from 'react' import PropTypes from 'prop-types' -import { withStyles, Select, MenuItem, Card, CardContent, CardHeader, makeStyles } from '@material-ui/core' +import { Select, MenuItem, Card, CardContent, CardHeader, makeStyles } from '@material-ui/core' import * as d3 from 'd3' import { scaleBand, scalePow } from 'd3-scale' import { formatQuantity, nomadPrimaryColor, nomadSecondaryColor } from '../../config.js' -import { searchContext } from './SearchContext' +import { searchContext } from './SearchContext.js' +import * as searchQuantities from '../../searchQuantities.json' const unprocessedLabel = 'not processed' const unavailableLabel = 'unavailable' @@ -17,135 +18,66 @@ function split(array, cols) { return [array.slice(0, size), ...split(array.slice(size), cols - 1)] } -const _mapping = { - 'energy_total': 'Total energy', - 'energy_total_T0': 'Total energy (0K)', - 'energy_free': 'Free energy', - 'energy_electrostatic': 'Electrostatic', - 'energy_X': 'Exchange', - 'energy_XC': 'Exchange-correlation', - 'energy_sum_eigenvalues': 'Band energy', - 'dos_values': 'DOS', - 'eigenvalues_values': 'Eigenvalues', - 'volumetric_data_values': 'Volumetric data', - 'electronic_kinetic_energy': 'Kinetic energy', - 'total_charge': 'Charge', - 'atom_forces_free': 'Free atomic forces', - 'atom_forces_raw': 'Raw atomic forces', - 'atom_forces_T0': 'Atomic forces (0K)', - 'atom_forces': 'Atomic forces', - 'stress_tensor': 'Stress tensor', - 'thermodynamical_property_heat_capacity_C_v': 'Heat capacity', - 'vibrational_free_energy_at_constant_volume': 'Free energy (const=V)', - 'band_energies': 'Band energies', - 'spin_S2': 'Spin momentum operator', - 'excitation_energies': 'Excitation energies', - 'oscillator_strengths': 'Oscillator strengths', - 'transition_dipole_moments': 'Transition dipole moments'} - -class QuantityHistogramUnstyled extends React.Component { - static propTypes = { - classes: PropTypes.object.isRequired, - title: PropTypes.string.isRequired, - width: PropTypes.number.isRequired, - data: PropTypes.object, - metric: PropTypes.string.isRequired, - value: PropTypes.string, - onChanged: PropTypes.func.isRequired, - defaultScale: PropTypes.number, - sort: PropTypes.bool, - tooltips: PropTypes.bool, - columns: PropTypes.number - } - - static styles = theme => ({ - root: {}, - content: { - paddingTop: 0, - position: 'relative' - }, - tooltip: { - textAlign: 'center', - position: 'absolute', - pointerEvents: 'none', - opacity: 0 - }, - tooltipContent: { - // copy of the material ui popper style - display: 'inline-block', - color: '#fff', - padding: '4px 8px', - fontSize: '0.625rem', - fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', - lineHeight: '1.4em', - borderRadius: '4px', - backgroundColor: '#616161' - } - }) - - constructor(props) { - super(props) - this.container = React.createRef() - } - - state = { - scalePower: this.props.defaultScale || 0.25 - } - - componentDidMount() { - this.updateChart() - } - - componentDidUpdate() { - this.updateChart() +const useStyles = makeStyles(theme => ({ + root: { + marginTop: theme.spacing(2) + }, + content: { + paddingTop: 0, + position: 'relative' + }, + tooltip: { + textAlign: 'center', + position: 'absolute', + pointerEvents: 'none', + opacity: 0 + }, + tooltipContent: { + // copy of the material ui popper style + display: 'inline-block', + color: '#fff', + padding: '4px 8px', + fontSize: '0.625rem', + fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', + lineHeight: '1.4em', + borderRadius: '4px', + backgroundColor: '#616161' } - - handleItemClicked(item) { - if (this.props.value === item.key) { - this.props.onChanged(null) - } else { - this.props.onChanged(item.key) - } +})) +export default function QuantityHistogram({ + quantity, initialScale = 1, valueLabels = {}, title, values, numberOfValues, + columns = 1, tooltips +}) { + title = title || quantity + values = values || (searchQuantities[quantity] && searchQuantities[quantity].statistic_values) + numberOfValues = numberOfValues || (values && values.length) || (searchQuantities[quantity] && searchQuantities[quantity].statistic_size) + const {response: {statistics, metric}, query, setQuery} = useContext(searchContext) + const statisticsData = statistics[quantity] + const classes = useStyles() + const containerRef = useRef() + const [scale, setScale] = useState(initialScale) + const handleItemClicked = item => { + setQuery({[quantity]: (query[quantity] === item.key) ? null : item.key}) } - updateChart() { - const {classes, sort, tooltips} = this.props + useEffect(() => { + let data = null - if (!this.props.data) { - return - } - - const data = Object.keys(this.props.data) - .map(key => ({ - key: key, - name: _mapping[key] || key, - value: this.props.data[key][this.props.metric] + if (!statistics[quantity]) { + data = [] + } else if (values) { + data = values.map(value => ({ + key: value, + name: valueLabels[value] || value, + value: statisticsData[value] ? statisticsData[value][metric] : 0 })) - - if (sort) { - data.sort((a, b) => { - const nameA = a.name - const nameB = b.name - - if (nameA === nameB) { - return 0 - } - - if (nameA === unprocessedLabel) { - return 1 - } - if (nameB === unprocessedLabel) { - return -1 - } - if (nameA === unavailableLabel) { - return 1 - } - if (nameB === unavailableLabel) { - return -1 - } - return nameA.localeCompare(nameB) - }) } else { + data = Object.keys(statisticsData) + .map(value => ({ + key: value, + name: valueLabels[value] || value, + value: statisticsData[value][metric] + })) // keep the data sorting, but put unavailable and not processed to the end const unavailableIndex = data.findIndex(d => d.name === unavailableLabel) const unprocessedIndex = data.findIndex(d => d.name === unprocessedLabel) @@ -157,21 +89,23 @@ class QuantityHistogramUnstyled extends React.Component { } } - const columns = this.props.columns || 1 + for (let i = data.length; i < numberOfValues; i++) { + data.push({key: `empty${i}`, name: '', value: 0}) + } + const columnSize = Math.ceil(data.length / columns) for (let i = data.length; i < columnSize * columns; i++) { data.push({key: `empty${i}`, name: '', value: 0}) } const columnsData = split(data, columns) - const {scalePower} = this.state - const selected = this.props.value + const selected = query[quantity] - const containerWidth = this.container.current.offsetWidth + const containerWidth = containerRef.current.offsetWidth const width = containerWidth / columns - (12 * (columns - 1)) const height = columnSize * 32 - const x = scalePow().range([0, width]).exponent(scalePower) + const x = scalePow().range([0, width]).exponent(scale) // we use at least the domain 0..1, because an empty domain causes a weird layout const max = d3.max(data, d => d.value) || 1 @@ -180,7 +114,7 @@ class QuantityHistogramUnstyled extends React.Component { const rectColor = d => selected === d.key ? nomadPrimaryColor.main : nomadSecondaryColor.light const textColor = d => selected === d.key ? '#FFF' : '#000' - const container = d3.select(this.container.current) + const container = d3.select(containerRef.current) const tooltip = container.select('.' + classes.tooltip) .style('width', width + 'px') .style('opacity', 0) @@ -203,14 +137,17 @@ class QuantityHistogramUnstyled extends React.Component { columnsData.forEach((data, i) => { const y = scaleBand().rangeRound([0, height]).padding(0.1) - y.domain(data.map(d => d.name)) + y.domain(data.map(d => d.key)) const items = svg.select('#column' + i) .selectAll('.item') - .data(data, d => d.name) + .data(data, d => d.key) items.exit().remove() + items + .on('click', d => handleItemClicked(d)) + let item = items.enter() .append('g') .attr('class', 'item') @@ -219,7 +156,7 @@ class QuantityHistogramUnstyled extends React.Component { item .append('rect') .attr('x', x(0)) - .attr('y', d => y(d.name)) + .attr('y', d => y(d.key)) .attr('width', width) .attr('class', 'background') .style('opacity', 0) @@ -229,7 +166,7 @@ class QuantityHistogramUnstyled extends React.Component { .append('rect') .attr('class', 'bar') .attr('x', x(0)) - .attr('y', d => y(d.name)) + .attr('y', d => y(d.key)) .attr('width', d => x(d.value) - x(0)) .attr('height', y.bandwidth()) .style('fill', rectColor) @@ -242,7 +179,7 @@ class QuantityHistogramUnstyled extends React.Component { .attr('class', 'name') .attr('dy', '.75em') .attr('x', x(0) + 4) - .attr('y', d => y(d.name) + 4) + .attr('y', d => y(d.key) + 4) .attr('text-anchor', 'start') .style('fill', textColor) .text(d => d.name) @@ -251,7 +188,7 @@ class QuantityHistogramUnstyled extends React.Component { .append('text') .attr('class', 'value') .attr('dy', y.bandwidth()) - .attr('y', d => y(d.name) - 4) + .attr('y', d => y(d.key) - 4) .attr('x', d => width - 4) .attr('text-anchor', 'end') .style('fill', textColor) @@ -259,7 +196,7 @@ class QuantityHistogramUnstyled extends React.Component { item .style('cursor', 'pointer') - .on('click', d => this.handleItemClicked(d)) + .on('click', d => handleItemClicked(d)) item .on('mouseover', function(d) { @@ -271,7 +208,7 @@ class QuantityHistogramUnstyled extends React.Component { .style('opacity', 1) tooltip .style('left', i * (width + 12) + 'px') - .style('top', (y(d.name) + 32) + 'px') + .style('top', (y(d.key) + 32) + 'px') tooltipContent.html(d.name) } }) @@ -289,7 +226,7 @@ class QuantityHistogramUnstyled extends React.Component { item .select('.bar') - .attr('y', d => y(d.name)) + .attr('y', d => y(d.key)) .attr('width', d => x(d.value) - x(0)) .attr('height', y.bandwidth()) .style('fill', rectColor) @@ -297,83 +234,88 @@ class QuantityHistogramUnstyled extends React.Component { item .select('.name') .text(d => d.name) - .attr('y', d => y(d.name) + 4) + .attr('y', d => y(d.key) + 4) .style('fill', textColor) item .select('.value') .text(d => formatQuantity(d.value)) - .attr('y', d => y(d.name) - 4) + .attr('y', d => y(d.key) - 4) .attr('x', width - 4) .style('fill', textColor) }) - } - - render() { - const { classes, title } = this.props - - return ( - <Card classes={{root: classes.root}}> - <CardHeader - title={title} - titleTypographyProps={{variant: 'body1'}} - action={( - <Select - value={this.state.scalePower} - onChange={(event) => this.setState({scalePower: event.target.value})} - displayEmpty - name="scale power" - > - <MenuItem value={1}>linear</MenuItem> - <MenuItem value={0.5}>1/2</MenuItem> - <MenuItem value={0.25}>1/4</MenuItem> - <MenuItem value={0.125}>1/8</MenuItem> - </Select> - )} - /> - <CardContent classes={{root: classes.content}}> - <div ref={this.container}> - <div className={classes.tooltip}> - <div className={classes.tooltipContent}></div> - </div> - <svg /> - </div> - </CardContent> - </Card> - ) - } -} - -export const QuantityHistogram = withStyles(QuantityHistogramUnstyled.styles)(QuantityHistogramUnstyled) - -const useQuantityStyles = makeStyles(theme => ({ - root: { - marginTop: theme.spacing(2) - } -})) + }) -export function Quantity(props) { - const classes = useQuantityStyles() - const {scale, quantity, title, data, ...restProps} = props - const {response, query, setQuery} = useContext(searchContext) - - const usedData = data || response.statistics[quantity] - - return <QuantityHistogram - classes={{root: classes.root}} - width={300} - defaultScale={scale || 1} - title={title || quantity} - data={usedData} - value={query[quantity]} - onChanged={selection => setQuery({...query, [quantity]: selection})} - {...restProps} /> + return <Card classes={{root: classes.root}}> + <CardHeader + title={title} + titleTypographyProps={{variant: 'body1'}} + action={( + <Select + value={scale} + onChange={(event) => setScale(event.target.value)} + displayEmpty + name="scale power" + > + <MenuItem value={1}>linear</MenuItem> + <MenuItem value={0.5}>1/2</MenuItem> + <MenuItem value={0.25}>1/4</MenuItem> + <MenuItem value={0.125}>1/8</MenuItem> + </Select> + )} + /> + <CardContent classes={{root: classes.content}}> + <div ref={containerRef}> + <div className={classes.tooltip}> + <div className={classes.tooltipContent}></div> + </div> + <svg /> + </div> + </CardContent> + </Card> } -Quantity.propTypes = { +QuantityHistogram.propTypes = { + /** + * The name of the search quantity that is displayed in the histogram. This has to + * match the provided statistics data. + */ quantity: PropTypes.string.isRequired, - metric: PropTypes.string.isRequired, + /** + * An optional title for the chart. If no title is given, the quantity is used. + */ title: PropTypes.string, - scale: PropTypes.number, - data: PropTypes.object + /** + * An optional scale power that is used as the initial scale before the user + * changes it. Default is 1 (linear scale). + */ + initialScale: PropTypes.number, + /** + * The data. Usually the statistics data send by NOMAD's API. + */ + data: PropTypes.object, + /** + * Optional list of possible values. This is used to sort the data and fill the data + * with 0-values to keep a persistent appearance, even if no data for that value exists. + * Otherwise, the values are not sorted. + */ + values: PropTypes.arrayOf(PropTypes.string), + /** + * The maximum number of values. This is used to fix the histograms size. Otherwise, + * the size is determined by the required space to render the existing values. + */ + numberOfValues: PropTypes.number, + /** + * An optional mapping between values and labels that should be used to render the + * values. + */ + valueLabels: PropTypes.object, + /** + * The number of columns that the values should be rendered in. Default is one. + */ + columns: PropTypes.number, + /** + * Set to true to enable tooltips for each value. + */ + tooltips: PropTypes.bool } diff --git a/gui/src/components/search/Search.js b/gui/src/components/search/Search.js index 17b90076c4..4c237c25fc 100644 --- a/gui/src/components/search/Search.js +++ b/gui/src/components/search/Search.js @@ -16,7 +16,7 @@ import GroupList from './GroupList' import ApiDialogButton from '../ApiDialogButton' import SearchIcon from '@material-ui/icons/Search' import UploadsChart from './UploadsChart' -import { Quantity } from './QuantityHistogram' +import QuantityHistogram from './QuantityHistogram' import SearchContext, { searchContext } from './SearchContext' import {objectFilter} from '../../utils' @@ -224,7 +224,7 @@ function UsersVisualization(props) { <UploadsChart metricsDefinitions={domain.searchMetrics}/> </CardContent> </Card> - <Quantity quantity="uploader" title="Uploaders" scale={1} metric={metric} /> + <QuantityHistogram quantity="uploader" title="Uploaders" /> </div> } diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index 2544dba533..ffa47f739f 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -51,7 +51,7 @@ export const searchContext = React.createContext() * constitute the actual search. This includes the domain and owner parameters. */ export default function SearchContext({initialRequest, initialQuery, query, children}) { - const defaultStatistics = ['atoms', 'authors'] + const defaultStatistics = [] // ['atoms', 'authors'] TODO const emptyResponse = { statistics: { total: { @@ -132,7 +132,7 @@ export default function SearchContext({initialRequest, initialQuery, query, chil ...requestRef.current.query, ...query } - api.search(apiQuery, requestRef.current.statisticsToRefresh) + api.search(apiQuery) .then(newResponse => { setResponse({...emptyResponse, ...newResponse, metric: metric}) }).catch(error => { @@ -174,10 +174,6 @@ export default function SearchContext({initialRequest, initialQuery, query, chil requestRef.current.groups = {...groups} }, [requestRef]) - const handleStatisticsToRefreshChange = statistics => { - requestRef.current.statisticsToRefresh = [...requestRef.current.statisticsToRefresh, statistics].filter(onlyUnique) - } - const handleQueryChange = (changes, replace) => { if (changes.atoms && changes.atoms.length === 0) { changes.atoms = undefined @@ -221,7 +217,7 @@ export default function SearchContext({initialRequest, initialQuery, query, chil setGroups: setGroups, setDomain: setDomain, setOwner: setOwner, - setStatisticsToRefresh: handleStatisticsToRefreshChange, + setStatisticsToRefresh: () => null, // TODO remove setStatistics: setStatistics, update: runRequest } diff --git a/gui/src/config.js b/gui/src/config.js index 3d97bec165..e5fdf1df5d 100644 --- a/gui/src/config.js +++ b/gui/src/config.js @@ -2,7 +2,7 @@ import { createMuiTheme } from '@material-ui/core' window.nomadEnv = window.nomadEnv || {} export const appBase = window.nomadEnv.appBase.replace(/\/$/, '') -export const apiBase = `${appBase}/api` +export const apiBase = 'http://labdev-nomad.esc.rzg.mpg.de/fairdi/nomad/testing-major/api' // `${appBase}/api` export const optimadeBase = `${appBase}/optimade` export const guiBase = process.env.PUBLIC_URL export const matomoUrl = window.nomadEnv.matomoUrl diff --git a/gui/src/searchQuantities.json b/gui/src/searchQuantities.json new file mode 100644 index 0000000000..c7d974cfa1 --- /dev/null +++ b/gui/src/searchQuantities.json @@ -0,0 +1,612 @@ +{ + "ems.chemical": { + "name": "ems.chemical", + "description": null, + "many": false, + "statistic_size": 10 + }, + "ems.sample_constituents": { + "name": "ems.sample_constituents", + "description": null, + "many": false, + "statistic_size": 10 + }, + "ems.sample_microstructure": { + "name": "ems.sample_microstructure", + "description": null, + "many": false, + "statistic_size": 10 + }, + "ems.experiment_summary": { + "name": "ems.experiment_summary", + "description": null, + "many": false, + "statistic_size": 10 + }, + "ems.experiment_location": { + "name": "ems.experiment_location", + "description": null, + "many": false, + "statistic_size": 10 + }, + "ems.experiment_time": { + "name": "ems.experiment_time", + "description": null, + "many": false, + "statistic_size": 10 + }, + "ems.method": { + "name": "ems.method", + "description": null, + "many": false, + "statistic_size": 10 + }, + "ems.probing_method": { + "name": "ems.probing_method", + "description": null, + "many": false, + "statistic_size": 10 + }, + "ems.repository_name": { + "name": "ems.repository_name", + "description": null, + "many": false, + "statistic_size": 10 + }, + "ems.repository_url": { + "name": "ems.repository_url", + "description": null, + "many": false, + "statistic_size": 10 + }, + "ems.entry_repository_url": { + "name": "ems.entry_repository_url", + "description": null, + "many": false, + "statistic_size": 10 + }, + "ems.preview_url": { + "name": "ems.preview_url", + "description": null, + "many": false, + "statistic_size": 10 + }, + "ems.quantities": { + "name": "ems.quantities", + "description": null, + "many": false, + "statistic_size": 10 + }, + "ems.group_hash": { + "name": "ems.group_hash", + "description": null, + "many": false, + "statistic_size": 10 + }, + "labels.label": { + "name": "labels.label", + "description": null, + "many": false, + "statistic_size": 10 + }, + "labels.type": { + "name": "labels.type", + "description": null, + "many": false, + "statistic_size": 10 + }, + "labels.source": { + "name": "labels.source", + "description": null, + "many": false, + "statistic_size": 10 + }, + "optimade.elements": { + "name": "optimade.elements", + "description": "Names of the different elements present in the structure.", + "many": false, + "statistic_size": 10 + }, + "optimade.nelements": { + "name": "optimade.nelements", + "description": "Number of different elements in the structure as an integer.", + "many": false, + "statistic_size": 10 + }, + "optimade.elements_ratios": { + "name": "optimade.elements_ratios", + "description": "Relative proportions of different elements in the structure.", + "many": false, + "statistic_size": 10 + }, + "optimade.chemical_formula_descriptive": { + "name": "optimade.chemical_formula_descriptive", + "description": "The chemical formula for a structure as a string in a form chosen by the API\nimplementation.", + "many": false, + "statistic_size": 10 + }, + "optimade.chemical_formula_reduced": { + "name": "optimade.chemical_formula_reduced", + "description": "The reduced chemical formula for a structure as a string with element symbols and\ninteger chemical proportion numbers. The proportion number MUST be omitted if it is 1.", + "many": false, + "statistic_size": 10 + }, + "optimade.chemical_formula_hill": { + "name": "optimade.chemical_formula_hill", + "description": "The chemical formula for a structure in Hill form with element symbols followed by\ninteger chemical proportion numbers. The proportion number MUST be omitted if it is 1.", + "many": false, + "statistic_size": 10 + }, + "optimade.chemical_formula_anonymous": { + "name": "optimade.chemical_formula_anonymous", + "description": "The anonymous formula is the chemical_formula_reduced, but where the elements are\ninstead first ordered by their chemical proportion number, and then, in order left to\nright, replaced by anonymous symbols A, B, C, ..., Z, Aa, Ba, ..., Za, Ab, Bb, ... and\nso on.", + "many": false, + "statistic_size": 10 + }, + "optimade.dimension_types": { + "name": "optimade.dimension_types", + "description": "List of three integers. For each of the three directions indicated by the three lattice\nvectors (see property lattice_vectors). This list indicates if the direction is\nperiodic (value 1) or non-periodic (value 0). Note: the elements in this list each\nrefer to the direction of the corresponding entry in lattice_vectors and not\nthe Cartesian x, y, z directions.", + "many": false, + "statistic_size": 10 + }, + "optimade.nsites": { + "name": "optimade.nsites", + "description": "An integer specifying the length of the cartesian_site_positions property.", + "many": false, + "statistic_size": 10 + }, + "optimade.structure_features": { + "name": "optimade.structure_features", + "description": "A list of strings that flag which special features are used by the structure.\n\n- disorder: This flag MUST be present if any one entry in the species list has a\nchemical_symbols list that is longer than 1 element.\n- unknown_positions: This flag MUST be present if at least one component of the\ncartesian_site_positions list of lists has value null.\n- assemblies: This flag MUST be present if the assemblies list is present.", + "many": false, + "statistic_size": 10 + }, + "dft.basis_set": { + "name": "dft.basis_set", + "description": "The used basis set functions.", + "many": false, + "statistic_size": 9, + "statistic_values": [ + "(L)APW+lo", + "FLAPW", + "gaussians", + "numeric AOs", + "plane waves", + "psinc functions", + "real-space grid", + "unavailable", + "not processed" + ] + }, + "dft.xc_functional": { + "name": "dft.xc_functional", + "description": "The libXC based xc functional classification used in the simulation.", + "many": false, + "statistic_size": 9, + "statistic_values": [ + "GGA", + "HF", + "OEP", + "hybrid", + "meta-GGA", + "vdW", + "LDA", + "unavailable", + "not processed" + ] + }, + "dft.system": { + "name": "dft.system", + "description": "The system type of the simulated system.", + "many": false, + "statistic_size": 8, + "statistic_values": [ + "1D", + "2D", + "atom", + "bulk", + "molecule / cluster", + "surface", + "unavailable", + "not processed" + ] + }, + "dft.compound_type": { + "name": "dft.compound_type", + "description": "The compound type of the simulated system.", + "many": false, + "statistic_size": 11, + "statistic_values": [ + "unary", + "binary", + "ternary", + "quaternary", + "quinary", + "sexinary", + "septenary", + "octanary", + "nonary", + "decinary", + "not processed" + ] + }, + "dft.crystal_system": { + "name": "dft.crystal_system", + "description": "The crystal system type of the simulated system.", + "many": false, + "statistic_size": 9, + "statistic_values": [ + "cubic", + "hexagonal", + "monoclinic", + "orthorombic", + "tetragonal", + "triclinic", + "trigonal", + "unavailable", + "not processed" + ] + }, + "dft.spacegroup": { + "name": "dft.spacegroup", + "description": "The spacegroup of the simulated system as number.", + "many": false, + "statistic_size": 10 + }, + "dft.spacegroup_symbol": { + "name": "dft.spacegroup_symbol", + "description": "The spacegroup as international short symbol.", + "many": false, + "statistic_size": 10 + }, + "dft.code_name": { + "name": "dft.code_name", + "description": "The name of the used code.", + "many": false, + "statistic_size": 36, + "statistic_values": [ + "ABINIT", + "ATK", + "BAND", + "BigDFT", + "CASTEP", + "CP2K", + "CPMD", + "Crystal", + "DL_POLY", + "DMol3", + "FHI-aims", + "GAMESS", + "GPAW", + "GPAW", + "Gaussian", + "MOLCAS", + "NWChem", + "ONETEP", + "ORCA", + "Octopus", + "Phonopy", + "Quantum Espresso", + "Siesta", + "VASP", + "VASP", + "WIEN2k", + "elastic", + "elk", + "exciting", + "fleur", + "gulp", + "libAtoms", + "qbox", + "turbomole", + "unavailable", + "not processed" + ] + }, + "dft.code_version": { + "name": "dft.code_version", + "description": "The version of the used code.", + "many": false, + "statistic_size": 10 + }, + "dft.n_geometries": { + "name": "dft.n_geometries", + "description": "Number of unique geometries.", + "many": false, + "statistic_size": 10 + }, + "dft.n_calculations": { + "name": "dft.n_calculations", + "description": "Number of single configuration calculation sections", + "many": false, + "statistic_size": 10 + }, + "dft.n_total_energies": { + "name": "dft.n_total_energies", + "description": "Number of total energy calculations", + "many": false, + "statistic_size": 10 + }, + "dft.n_quantities": { + "name": "dft.n_quantities", + "description": "Number of metainfo quantities parsed from the entry.", + "many": false, + "statistic_size": 10 + }, + "dft.quantities": { + "name": "dft.quantities", + "description": "All quantities that are used by this entry.", + "many": true, + "statistic_size": 10 + }, + "dft.searchable_quantities": { + "name": "dft.searchable_quantities", + "description": "All quantities with existence filters in the search GUI.", + "many": true, + "statistic_size": 25 + }, + "dft.geometries": { + "name": "dft.geometries", + "description": "Hashes for each simulated geometry", + "many": false, + "statistic_size": 10 + }, + "dft.group_hash": { + "name": "dft.group_hash", + "description": "Hashes that describe unique geometries simulated by this code run.", + "many": true, + "statistic_size": 10 + }, + "dft.labels_springer_compound_class": { + "name": "dft.labels_springer_compound_class", + "description": "Springer compund classification.", + "many": true, + "statistic_size": 20 + }, + "dft.labels_springer_classification": { + "name": "dft.labels_springer_classification", + "description": "Springer classification by property.", + "many": true, + "statistic_size": 10 + }, + "upload_id": { + "name": "upload_id", + "description": "A random UUID that uniquely identifies the upload of the entry.", + "many": true, + "statistic_size": 10 + }, + "calc_id": { + "name": "calc_id", + "description": "A unique ID based on the upload id and entry's mainfile.", + "many": true, + "statistic_size": 10 + }, + "calc_hash": { + "name": "calc_hash", + "description": "A raw file content based checksum/hash.", + "many": true, + "statistic_size": 10 + }, + "mainfile": { + "name": "mainfile", + "description": "Search within the mainfile path.", + "many": true, + "statistic_size": 10 + }, + "mainfile_path": { + "name": "mainfile_path", + "description": "Search for the exact mainfile.", + "many": true, + "statistic_size": 10 + }, + "path": { + "name": "path", + "description": "Search within the paths.", + "many": false, + "statistic_size": 10 + }, + "files": { + "name": "files", + "description": "Search for exact paths.", + "many": true, + "statistic_size": 10 + }, + "pid": { + "name": "pid", + "description": "The unique, sequentially enumerated, integer persistent identifier", + "many": true, + "statistic_size": 10 + }, + "raw_id": { + "name": "raw_id", + "description": "A raw format specific id that was acquired from the files of this entry", + "many": true, + "statistic_size": 10 + }, + "domain": { + "name": "domain", + "description": "The material science domain", + "many": false, + "statistic_size": 10 + }, + "published": { + "name": "published", + "description": "Indicates if the entry is published", + "many": false, + "statistic_size": 10 + }, + "processed": { + "name": "processed", + "description": "Indicates that the entry is successfully processed.", + "many": false, + "statistic_size": 10 + }, + "last_processing": { + "name": "last_processing", + "description": "The datetime of the last attempted processing.", + "many": false, + "statistic_size": 10 + }, + "nomad_version": { + "name": "nomad_version", + "description": "The NOMAD version used for the last processing attempt.", + "many": true, + "statistic_size": 10 + }, + "nomad_commit": { + "name": "nomad_commit", + "description": "The NOMAD commit used for the last processing attempt.", + "many": true, + "statistic_size": 10 + }, + "parser_name": { + "name": "parser_name", + "description": "The NOMAD parser used for the last processing attempt.", + "many": true, + "statistic_size": 10 + }, + "comment": { + "name": "comment", + "description": "A user provided comment.", + "many": false, + "statistic_size": 10 + }, + "references": { + "name": "references", + "description": "User provided references (URLs).", + "many": false, + "statistic_size": 10 + }, + "uploader.user_id": { + "name": "uploader.user_id", + "description": "The unique, persistent keycloak UUID", + "many": false, + "statistic_size": 10 + }, + "uploader.name": { + "name": "uploader.name", + "description": null, + "many": false, + "statistic_size": 10 + }, + "uploader.email": { + "name": "uploader.email", + "description": null, + "many": false, + "statistic_size": 10 + }, + "uploader": { + "name": "uploader", + "description": "Search uploader with exact names.", + "many": true, + "statistic_size": 10 + }, + "uploader_id": { + "name": "uploader_id", + "description": "The uploader of the entry", + "many": false, + "statistic_size": 10 + }, + "authors": { + "name": "authors", + "description": "Search authors with exact names.", + "many": true, + "statistic_size": 1000 + }, + "owners": { + "name": "owners", + "description": "Search owner with exact names.", + "many": true, + "statistic_size": 10 + }, + "with_embargo": { + "name": "with_embargo", + "description": "Indicated if this entry is under an embargo", + "many": false, + "statistic_size": 10 + }, + "upload_time": { + "name": "upload_time", + "description": "The datetime this entry was uploaded to nomad", + "many": false, + "statistic_size": 10 + }, + "upload_name": { + "name": "upload_name", + "description": "The user provided upload name", + "many": true, + "statistic_size": 10 + }, + "datasets.dataset_id": { + "name": "datasets.dataset_id", + "description": "The unique identifier for this dataset as a string. It should be\na randomly generated UUID, similar to other nomad ids.", + "many": false, + "statistic_size": 10 + }, + "datasets.name": { + "name": "datasets.name", + "description": "The human readable name of the dataset as string. The dataset name must be\nunique for the user.", + "many": false, + "statistic_size": 10 + }, + "datasets.doi": { + "name": "datasets.doi", + "description": "The optional Document Object Identifier (DOI) associated with this dataset.\nNomad can register DOIs that link back to the respective representation of\nthe dataset in the nomad UI. This quantity holds the string representation of\nthis DOI. There is only one per dataset. The DOI is just the DOI name, not its\nfull URL, e.g. \"10.17172/nomad/2019.10.29-1\".", + "many": false, + "statistic_size": 10 + }, + "datasets.created": { + "name": "datasets.created", + "description": "The date when the dataset was first created.", + "many": false, + "statistic_size": 10 + }, + "datasets": { + "name": "datasets", + "description": "Search for a particular dataset by exact name.", + "many": true, + "statistic_size": 10 + }, + "dataset_id": { + "name": "dataset_id", + "description": "Search for a particular dataset by its id.", + "many": true, + "statistic_size": 10 + }, + "external_id": { + "name": "external_id", + "description": "A user provided external id.", + "many": true, + "statistic_size": 10 + }, + "last_edit": { + "name": "last_edit", + "description": "The datetime the user metadata was edited last.", + "many": false, + "statistic_size": 10 + }, + "formula": { + "name": "formula", + "description": "A (reduced) chemical formula.", + "many": false, + "statistic_size": 10 + }, + "atoms": { + "name": "atoms", + "description": "The atom labels of all atoms of the entry's material.", + "many": true, + "statistic_size": 119 + }, + "only_atoms": { + "name": "only_atoms", + "description": "The atom labels concatenated in order-number order.", + "many": true, + "statistic_size": 10 + }, + "n_atoms": { + "name": "n_atoms", + "description": "The number of atoms in the entry's material", + "many": false, + "statistic_size": 10 + } +} diff --git a/nomad/datamodel/dft.py b/nomad/datamodel/dft.py index eb5423a4b3..1b1f8e31d9 100644 --- a/nomad/datamodel/dft.py +++ b/nomad/datamodel/dft.py @@ -147,28 +147,38 @@ class DFTMetadata(MSection): basis_set = Quantity( type=str, default='not processed', description='The used basis set functions.', - a_search=Search(statistic_size=20)) + a_search=Search(statistic_values=[ + '(L)APW+lo', 'FLAPW', 'gaussians', 'numeric AOs', 'plane waves', 'psinc functions', + 'real-space grid', 'unavailable', 'not processed' + ])) xc_functional = Quantity( type=str, default='not processed', description='The libXC based xc functional classification used in the simulation.', - a_search=Search(statistic_size=20)) + a_search=Search(statistic_values=list(xc_treatments.values()) + ['unavailable', 'not processed'])) system = Quantity( type=str, default='not processed', description='The system type of the simulated system.', - a_search=Search()) + a_search=Search(statistic_values=[ + '1D', '2D', 'atom', 'bulk', 'molecule / cluster', 'surface', + 'unavailable', 'not processed' + ])) compound_type = Quantity( type=str, default='not processed', description='The compound type of the simulated system.', - a_search=Search(statistic_size=11) + a_search=Search(statistic_values=compound_types + ['not processed']) ) crystal_system = Quantity( type=str, default='not processed', description='The crystal system type of the simulated system.', - a_search=Search()) + a_search=Search( + statistic_values=[ + 'cubic', 'hexagonal', 'monoclinic', 'orthorombic', 'tetragonal', + 'triclinic', 'trigonal', 'unavailable', 'not processed'] + )) spacegroup = Quantity( type=int, default=-1, @@ -183,7 +193,7 @@ class DFTMetadata(MSection): code_name = Quantity( type=str, default='not processed', description='The name of the used code.', - a_search=Search(statistic_size=40)) + a_search=Search()) # in import the parser module is added codes here as statistic_values code_version = Quantity( type=str, default='not processed', @@ -215,7 +225,7 @@ class DFTMetadata(MSection): searchable_quantities = Quantity( type=str, shape=['0..*'], - description='Energy-related quantities.', + description='All quantities with existence filters in the search GUI.', a_search=Search(many_and='append', statistic_size=len(_searchable_quantities))) geometries = Quantity( @@ -237,7 +247,7 @@ class DFTMetadata(MSection): type=str, shape=['0..*'], description='Springer compund classification.', a_search=Search( - many_and='append', statistic_size=10, + many_and='append', statistic_size=20, statistic_order='_count')) labels_springer_classification = Quantity( diff --git a/nomad/gitinfo.py b/nomad/gitinfo.py index 040871d98f..f8f3740d75 100644 --- a/nomad/gitinfo.py +++ b/nomad/gitinfo.py @@ -1 +1 @@ -log, ref, version, commit = "2b2f02a WIP: Refactored some of the search. Added docs for components.", "heads/mui4", "v0.7.9-396-g2b2f02a", "2b2f02a" +log, ref, version, commit = "44c7e69 Fixed performance and other isseus.", "heads/mui4", "v0.7.9-398-g44c7e69", "44c7e69" diff --git a/nomad/metainfo/search_extension.py b/nomad/metainfo/search_extension.py index 0f4db58d56..ce95ea1dc2 100644 --- a/nomad/metainfo/search_extension.py +++ b/nomad/metainfo/search_extension.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Callable, Any, Dict +from typing import Callable, Any, Dict, List from nomad.metainfo.elastic_extension import Elastic @@ -63,6 +63,10 @@ class Search(Elastic): statistics_order: The order key that is passed to elastic search to determine the order of the statistic values. + statistics_values: + If the statistics has a fixed set of values, use this parameter to set it + as a list of strings. Will fill statistics_size with the len of this list. + The information can be used (e.g. by the GUI) to fill in empty values. group: Indicates that his quantity can be used to group results. The value will be the name of the group. search_field: The qualified field in the elastic mapping that is used to search. @@ -80,6 +84,7 @@ class Search(Elastic): group: str = None, metric: str = None, metric_name: str = None, statistic_size: int = 10, statistic_order: str = '_key', + statistic_values: List[str] = None, derived: Callable[[Any], Any] = None, search_field: str = None, **kwargs): @@ -96,6 +101,7 @@ class Search(Elastic): self.metric_name = metric_name self.statistic_size = statistic_size self.statistic_order = statistic_order + self.statistic_values = statistic_values self.search_field = search_field self.derived = derived @@ -174,3 +180,13 @@ class Search(Elastic): return fields.List(value_field(), description=self.description) else: return value_field(description=self.description) + + @property + def statistic_values(self): + return self._statistic_values + + @statistic_values.setter + def statistic_values(self, values): + self._statistic_values = values + if self._statistic_values is not None: + self.statistic_size = len(self._statistic_values) diff --git a/nomad/parsing/__init__.py b/nomad/parsing/__init__.py index 0ca7b8072c..45ce4082d9 100644 --- a/nomad/parsing/__init__.py +++ b/nomad/parsing/__init__.py @@ -73,7 +73,7 @@ based on nomad@fairdi's metainfo: from typing import Callable, IO, Union, Dict import os.path -from nomad import config +from nomad import config, datamodel from nomad.parsing.legacy import ( AbstractParserBackend, Backend, BackendError, BadContextUri, LegacyParser, VaspOutcarParser) @@ -504,3 +504,10 @@ parser_dict['parser/fleur'] = parser_dict['parsers/fleur'] parser_dict['parser/molcas'] = parser_dict['parsers/molcas'] parser_dict['parser/octopus'] = parser_dict['parsers/octopus'] parser_dict['parser/onetep'] = parser_dict['parsers/onetep'] + +# register code names as possible statistic value to the dft datamodel +code_names = sorted([ + getattr(parser, 'code_name') + for parser in parsers + if parser.domain == 'dft' and getattr(parser, 'code_name', None) is not None and getattr(parser, 'code_name') != 'currupted mainfile']) +datamodel.DFTMetadata.code_name.a_search.statistic_values = code_names + [config.services.unavailable_value, config.services.not_processed_value] diff --git a/nomad/search.py b/nomad/search.py index 7e22058a18..7b1499c606 100644 --- a/nomad/search.py +++ b/nomad/search.py @@ -665,14 +665,27 @@ def flat(obj, prefix=None): if __name__ == '__main__': + # Due to this import, the parsing module will register all code_names based on parser + # implementations. + from nomad import parsing # pylint: disable=unused-import import json - export = { - search_quantity.qualified_name: { + def to_dict(search_quantity): + result = { 'name': search_quantity.qualified_name, 'description': search_quantity.description, - 'many': search_quantity.many + 'many': search_quantity.many, } + + if search_quantity.statistic_size > 0: + result['statistic_size'] = search_quantity.statistic_size + if search_quantity.statistic_values is not None: + result['statistic_values'] = search_quantity.statistic_values + + return result + + export = { + search_quantity.qualified_name: to_dict(search_quantity) for search_quantity in search_quantities.values() } print(json.dumps(export, indent=2)) -- GitLab