From 5d4424f1d7c484d24c24ce80b5ab9c4fb50ae55d Mon Sep 17 00:00:00 2001 From: Alvin Noe Ladines <ladinesalvinnoe@gmail.com> Date: Thu, 19 Mar 2020 18:39:22 +0100 Subject: [PATCH] (re-)Implemented properties and users tabs --- .../components/dft/DFTSearchAggregations.js | 10 +- .../dft/DFTSearchByPropertyAggregations.js | 53 +++ gui/src/components/domains.js | 5 + .../components/search/QuantityHistogram.js | 35 +- gui/src/components/search/Search.js | 49 ++ gui/src/components/search/SearchContext.js | 3 +- gui/src/components/search/UploadsChart.js | 434 ++++++++++++++++++ nomad/app/api/repo.py | 5 +- nomad/datamodel/datamodel.py | 2 +- nomad/datamodel/dft.py | 132 ++++++ nomad/search.py | 4 +- 11 files changed, 722 insertions(+), 10 deletions(-) create mode 100644 gui/src/components/dft/DFTSearchByPropertyAggregations.js create mode 100644 gui/src/components/search/UploadsChart.js diff --git a/gui/src/components/dft/DFTSearchAggregations.js b/gui/src/components/dft/DFTSearchAggregations.js index f2d0dbee1e..a281d29324 100644 --- a/gui/src/components/dft/DFTSearchAggregations.js +++ b/gui/src/components/dft/DFTSearchAggregations.js @@ -36,13 +36,15 @@ class DFTSearchAggregations extends React.Component { <Grid item xs={4}> <Quantity quantity="dft.code_name" title="Code" scale={0.25} metric={usedMetric} /> </Grid> - <Grid item xs={4}> - <Quantity quantity="dft.system" title="System type" scale={0.25} metric={usedMetric} /> - <Quantity quantity="dft.crystal_system" title="Crystal system" scale={1} metric={usedMetric} /> - </Grid> <Grid item xs={4}> <Quantity quantity="dft.basis_set" title="Basis set" scale={0.25} metric={usedMetric} /> <Quantity quantity="dft.xc_functional" title="XC functionals" scale={0.5} metric={usedMetric} /> + <Quantity quantity="dft.compound_type" title="Compound type" scale={1} metric={usedMetric} /> + </Grid> + <Grid item xs={4}> + <Quantity quantity="dft.system" title="System type" scale={0.25} metric={usedMetric} /> + <Quantity quantity="dft.crystal_system" title="Crystal system" scale={1} metric={usedMetric} /> + <Quantity quantity="dft.labels_springer_compound_class" title="Springer compound class" scale={1} metric={usedMetric} /> </Grid> </Grid> ) diff --git a/gui/src/components/dft/DFTSearchByPropertyAggregations.js b/gui/src/components/dft/DFTSearchByPropertyAggregations.js new file mode 100644 index 0000000000..4ebc08fa32 --- /dev/null +++ b/gui/src/components/dft/DFTSearchByPropertyAggregations.js @@ -0,0 +1,53 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Grid } from '@material-ui/core' +import { Quantity } from '../search/QuantityHistogram' +import SearchContext from '../search/SearchContext' +import { withApi } from '../api' + +class DFTSearchByPropertyAggregations extends React.Component { + static propTypes = { + info: PropTypes.object + } + + static contextType = SearchContext.type + + render() { + const {info} = this.props + const {state: {response: {statistics}, usedMetric}} = this.context + + if (statistics.code_name && info) { + // filter based on known codes, since elastic search might return 0 aggregations on + // obsolete code names + const filteredCodeNames = {} + const defaultValue = { + code_runs: 0 + } + defaultValue[usedMetric] = 0 + info.codes.forEach(key => { + filteredCodeNames[key] = statistics.code_name[key] || defaultValue + }) + statistics.code_name = filteredCodeNames + } + + return ( + <Grid container spacing={24}> + <Grid item xs={4}> + <Quantity quantity="dft.quantities_energy" title="Energy" scale={1} metric={usedMetric} /> + <Quantity quantity="dft.quantities_forces" title="Forces" scale={1} metric={usedMetric} /> + <Quantity quantity="dft.quantities_electronic" title="Electronic" scale={1} metric={usedMetric} /> + </Grid> + <Grid item xs={4}> + <Quantity quantity="dft.quantities_magnetic" title="Magnetic" scale={1} metric={usedMetric} /> + <Quantity quantity="dft.quantities_vibrational" title="Vibrational" scale={1} metric={usedMetric} /> + <Quantity quantity="dft.quantities_optical" title="Optical" scale={1} metric={usedMetric} /> + </Grid> + <Grid item xs={4}> + <Quantity quantity="dft.labels_springer_classification" title="Springer classification" scale={1} metric={usedMetric} /> + </Grid> + </Grid> + ) + } +} + +export default withApi(false, false)(DFTSearchByPropertyAggregations) diff --git a/gui/src/components/domains.js b/gui/src/components/domains.js index 5c3e6c631d..8520a02891 100644 --- a/gui/src/components/domains.js +++ b/gui/src/components/domains.js @@ -5,6 +5,7 @@ import DFTEntryCards from './dft/DFTEntryCards' import EMSSearchAggregations from './ems/EMSSearchAggregations' import EMSEntryOverview from './ems/EMSEntryOverview' import EMSEntryCards from './ems/EMSEntryCards' +import DFTSearchByPropertyAggregations from './dft/DFTSearchByPropertyAggregations' export const domains = ({ dft: { @@ -23,6 +24,10 @@ export const domains = ({ * onChange (callback to propagate searchValue changes). */ SearchAggregations: DFTSearchAggregations, + /** + * A component that is used to render the search aggregations by property. + */ + SearchByPropertyAggregations: DFTSearchByPropertyAggregations, /** * Metrics are used to show values for aggregations. Each metric has a key (used * for API calls), a label (used in the select form), and result string (to show diff --git a/gui/src/components/search/QuantityHistogram.js b/gui/src/components/search/QuantityHistogram.js index 4b1b433597..b6e430da01 100644 --- a/gui/src/components/search/QuantityHistogram.js +++ b/gui/src/components/search/QuantityHistogram.js @@ -9,6 +9,39 @@ import SearchContext from '../search/SearchContext' const unprocessed_label = 'not processed' const unavailable_label = 'unavailable' +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'} + +function map_key (name) { + if (name in _mapping) { + return _mapping[name] + } + return name +} + class QuantityHistogramUnstyled extends React.Component { static propTypes = { classes: PropTypes.object.isRequired, @@ -69,7 +102,7 @@ class QuantityHistogramUnstyled extends React.Component { const data = Object.keys(this.props.data) .map(key => ({ - name: key, + name: map_key(key), value: this.props.data[key][this.props.metric] })) diff --git a/gui/src/components/search/Search.js b/gui/src/components/search/Search.js index 00684af68f..287238d92b 100644 --- a/gui/src/components/search/Search.js +++ b/gui/src/components/search/Search.js @@ -16,6 +16,7 @@ import UploadList from './UploadsList' import GroupList from './GroupList' import ApiDialogButton from '../ApiDialogButton' import SearchIcon from '@material-ui/icons/Search' +import UploadsChart from './UploadsChart' class Search extends React.Component { static tabs = { @@ -95,6 +96,16 @@ class Search extends React.Component { render: props => <DomainVisualization {...props}/>, label: 'Meta data', description: 'Shows histograms on key metadata' + }, + 'property': { + render: props => <PropertyVisualization {...props}/>, + label: 'Properties', + description: 'Shows histograms on key properties' + }, + 'users': { + render: props => <UsersVisualization {...props}/>, + label: 'Users', + description: 'Show statistics on user metadata' } } @@ -215,6 +226,44 @@ class DomainVisualization extends React.Component { } } +class PropertyVisualization extends React.Component { + static propTypes = { + open: PropTypes.bool + } + + static contextType = SearchContext.type + + render() { + const {domain} = this.context.state + const {open} = this.props + + return <KeepState visible={open} render={() => + <domain.SearchByPropertyAggregations /> + }/> + } +} + +class UsersVisualization extends React.Component { + static propTypes = { + open: PropTypes.bool + } + + static contextType = SearchContext.type + + render () { + const {domain} = this.context.state + const {open} = this.props + + return <KeepState visible={open} render={() => + <Card> + <CardContent> + <UploadsChart metricsDefinitions={domain.searchMetrics}/> + </CardContent> + </Card> + }/> + } +} + class ElementsVisualization extends React.Component { static propTypes = { open: PropTypes.bool diff --git a/gui/src/components/search/SearchContext.js b/gui/src/components/search/SearchContext.js index d189ae718d..3ef869c8ef 100644 --- a/gui/src/components/search/SearchContext.js +++ b/gui/src/components/search/SearchContext.js @@ -50,7 +50,8 @@ class SearchContext extends React.Component { order_by: 'upload_time', order: -1, page: 1, - per_page: 10 + per_page: 10, + date_histogram: true }, metric: this.defaultMetric, usedMetric: this.defaultMetric, diff --git a/gui/src/components/search/UploadsChart.js b/gui/src/components/search/UploadsChart.js new file mode 100644 index 0000000000..ca8df54c47 --- /dev/null +++ b/gui/src/components/search/UploadsChart.js @@ -0,0 +1,434 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { withStyles, Select, MenuItem } from '@material-ui/core' +import Grid from '@material-ui/core/Grid' +import TextField from '@material-ui/core/TextField' +import * as d3 from 'd3' +import { scaleBand, scalePow } from 'd3-scale' +import { nomadSecondaryColor } from '../../config.js' +import SearchContext from './SearchContext' +import { compose } from 'recompose' +import { withApi } from '../api' +import { Quantity } from './QuantityHistogram' + +class UploadsHistogramUnstyled extends React.Component { + static propTypes = { + classes: PropTypes.object.isRequired, + height: PropTypes.number.isRequired, + data: PropTypes.object, + interval: PropTypes.string, + metric: PropTypes.string.isRequired, + metricsDefinitions: PropTypes.object.isRequired, + onChanged: PropTypes.func.isRequired, + defaultScale: PropTypes.number + } + + static styles = theme => ({ + root: {}, + content: { + paddingTop: 10 + } + }) + + constructor(props) { + super(props) + this.state = { + scalePower: this.props.defaultScale || 1.0, + interval: this.props.interval || '1M', + time: null, + from_time: 0, + until_time: 0 + } + + this.container = React.createRef() + this.svgEl = React.createRef() + } + + startDate = '2013-01-01' + + scales = [ + { + label: 'Linear', + value: 1.0 + }, + { + label: '1/2', + value: 0.5 + }, + { + label: '1/4', + value: 0.25 + }, + { + label: '1/8', + value: 0.25 + } + ] + + intervals = [ + { + label: 'Yearly', + value: '1y', + number: 31536000000 + }, + { + label: 'Monthly', + value: '1M', + number: 2678400000 + }, + { + label: 'Daily', + value: '1d', + number: 86400000 + }, + { + label: 'Hourly', + value: '1h', + number: 3600000 + }, + { + label: 'Minute', + value: '1m', + number: 60000 + }, + { + label: 'Second', + value: '1s', + number: 1000 + } + ] + + timeInterval = Object.assign({}, ...this.intervals.map(e => ( {[e.value]: e.number}))) + + componentDidMount() { + const from_time = new Date(this.startDate).getTime() + const until_time = new Date().getTime() + this.handleTimeChange(from_time, 'from_time', 'all') + this.handleTimeChange(until_time, 'until_time', 'all') + } + + componentDidUpdate() { + this.updateChart() + } + + handleQueryChange() { + const interval = this.state.interval + const from_time = new Date(this.state.from_time) + const until_time = new Date(this.state.until_time) + this.props.onChanged(from_time.toISOString(), until_time.toISOString(), interval) + } + + handleIntervalChange(newInterval) { + // TODO: add a refresh button so directly updating interval is not necessary + this.state.interval = newInterval + //this.setState({interval: newInterval}) + this.handleQueryChange() + } + + handleTimeChange(newTime, key, target) { + let date + if (!newTime) { + date = key === 'from_time' ? new Date(this.startDate) : new Date() + } else { + date = new Date(newTime) + } + if (target === 'state' || target === 'all') { + key === 'from_time' ? this.setState({from_time: date.getTime()}) : this.setState({until_time: date.getTime()}) + } + if (target === 'picker' || target === 'all') { + document.getElementById(key).value = date.toISOString().substring(0,10) + } + } + + handleItemClicked(item) { + const selected = item.time + if (selected === this.state.time) { + this.props.onChanged(null, null, null) + } else { + const deltaT = this.timeInterval[this.state.interval] + this.handleTimeChange(selected, 'from_time', 'all') + this.handleTimeChange(selected + deltaT, 'until_time', 'all') + this.handleQueryChange() + } + } + + resolveDate (name) { + const date = new Date(parseInt(name, 10)) + const year = date.toLocaleDateString(undefined, {year: 'numeric'}) + const month = date.toLocaleDateString(undefined, {month: 'short'}) + const day = date.toLocaleDateString(undefined, {day: 'numeric'}) + const hour = date.toLocaleTimeString(undefined, {hour: 'numeric'}) + const min = date.toLocaleTimeString(undefined, {minute: 'numeric'}) + const sec= date.toLocaleTimeString(undefined, {second: 'numeric'}) + + const intervals = { + '1y': year, + '1M': month, + '1d': day, + '1h': hour, + '1m': min, + '1s': sec + } + + return intervals[this.state.interval] + } + + hover (svg, bar) { + const textOffset = 25 + + const tooltip = svg.append('g') + .attr('class', 'tooltip') + .style('display', 'none') + + const hoverBox = tooltip.append('rect') + .attr('width', 10) + .attr('height', 20) + .attr('fill', 'white') + .style('opacity', 0.0) + + const text = tooltip.append('text') + .attr('x', textOffset) + .attr('dy', '1.2em') + .style('text-anchor', 'start') + .attr('font-size', '12px') + //.attr('font-weight', 'bold') + + bar + .on('mouseover', () => { + tooltip.style('display', null) + let { width } = text.node().getBBox() + hoverBox.attr('width', `${ width + textOffset }px`) + }) + .on('mouseout', () => tooltip.style('display', 'none')) + .on('mousemove', function(d) { + let xPosition = d3.mouse(this)[0] - 15 + let yPosition = d3.mouse(this)[1] - 25 + + tooltip.attr('transform', `translate( ${ xPosition }, ${ yPosition })`) + tooltip.attr('data-html', 'true') + tooltip.select('text').text( new Date(d.time).toISOString() + ': ' + d.value ) + }) + } + + updateChart () { + let data = [] + if (! this.props.data) { + return + } else { + data = Object.keys(this.props.data).map(key => ({ + time: parseInt(key, 10), + name: this.resolveDate(key), + value: this.props.data[key][this.props.metric] + }))} + + + data.sort((a, b) => d3.ascending(a.time, b.time)) + if (data.length > 0) { + this.handleTimeChange(this.state.from_time, 'from_time', 'picker') + this.handleTimeChange(this.state.until_time, 'until_time', 'picker') + } + + const scalePower = this.state.scalePower + const width = this.container.current.offsetWidth + const height = this.props.height + const margin = Math.round(0.1*height) + + const x = scaleBand().rangeRound([margin, width]).padding(0.1) + const y = scalePow().range([height-margin, margin]).exponent(scalePower) + + const max = d3.max(data, d => d.value) || 0 + x.domain(data.map(d => d.name)) + y.domain([0, max]) + + let svg = d3.select(this.svgEl.current) + svg.attr('width', width) + svg.attr('height', height) + + const xAxis = d3.axisBottom(x) + svg.select('.xaxis').remove() + svg.append('g') + .attr('transform', `translate(0,${height-margin})`) + .attr('class', 'xaxis') + .call(xAxis) + + svg.select('.xlabel').remove() + svg.append('text') + .attr('class', 'xlabel') + .attr("x", width) + .attr("y", height-4) + .attr('dy', ".35em") + .attr('font-size', '12px') + .style('text-anchor', 'end') + + const yAxis = d3.axisLeft(y) + svg.select('.yaxis').remove() + svg.append('g') + .attr('transform', `translate(${margin}, 0)`) + .attr('class', 'yaxis') + .call(yAxis) + + const {label, shortLabel} = this.props.metricsDefinitions[this.props.metric] + svg.select('.ylabel').remove() + svg.append('text') + .attr('class','ylabel') + .attr('x', 0) + .attr('y', 0) + .attr('dy', "1em") + .attr('text-anchor', 'start') + .attr('font-size', '12px') + .text(`${shortLabel ? shortLabel : label}`) + + let withData = svg + .selectAll('.bar').remove().exit() + .data(data) + + let item = withData.enter() + .append('g') + + item + .append('rect') + .attr('class', 'bar') + .attr('x', d => x(d.name)) + .attr('y', d => y(d.value)) + .attr('width', x.bandwidth()) + .attr('height', d => y(0) - y(d.value)) + .style('fill', nomadSecondaryColor.light) + + item + .style('cursor', 'pointer') + .on('click', d => this.handleItemClicked(d)) + + svg.select('.tooltip').remove() + svg.call(this.hover, item) +} + + render () { + return ( + <div> + <Grid container justify='space-around' spacing={24}> + <Grid item xs={2}> + <Select + margin='none' + id='scales' + value={this.state.scalePower} + onChange={(event) => this.setState({scalePower: event.target.value})} + label= 'scale' + > {this.scales.map(item => ( + <MenuItem + value={item.value} + key={item.label}> {item.label} + </MenuItem>))} + </Select> + </Grid> + <Grid item xs={3}> + <TextField + id='from_time' + label="from time" + type="date" + onChange={(event) => this.handleTimeChange(event.target.value, 'from_time', 'state')} + InputLabelProps={{ + shrink: true, + }} + /> + </Grid> + <Grid item xs={3}> + <Select + id='interval' + value={this.state.interval} + onChange={(event) => this.handleIntervalChange(event.target.value)} + label= 'interval' + > {this.intervals.map(item => ( + <MenuItem value={item.value} key={item.value}> + {item.label} + </MenuItem>))} + </Select> + </Grid> + <Grid item xs={3}> + <TextField + id='until_time' + label="until time" + type="date" + onChange={(event) => this.handleTimeChange(event.target.value, 'until_time', 'state')} + InputLabelProps={{ + shrink: true, + }} + /> + </Grid> + </Grid> + <div ref={this.container}> + <svg ref={this.svgEl}></svg> + </div> + </div> + ) + } +} + +export const UploadsHistogram = withStyles(UploadsHistogramUnstyled.styles)(UploadsHistogramUnstyled) + +class UploadersListUnstyled extends React.Component { + + static propTypes = { + classes: PropTypes.object.isRequired + } + + static styles = theme => ({ + root: { + marginTop: theme.spacing.unit * 2 + } + }) + + static contextType = SearchContext.type + + render () { + const {state: {usedMetric}} = this.context + + return ( + <Grid> + <Quantity quantity="uploader" title="Top Uploaders" scale={1} metric={usedMetric} /> + </Grid> + ) + } +} + +export const UploadersList = withStyles(UploadersListUnstyled.styles)(UploadersListUnstyled) + +class UploadsChart extends React.Component { + static propTypes = { + classes: PropTypes.object.isRequired, + metricsDefinitions: PropTypes.object.isRequired + } + static styles = theme => ({ + root: { + marginTop: theme.spacing.unit + } + }) + + static contextType = SearchContext.type + + render() { + const {classes, metricsDefinitions, ...props} = this.props + const {state: {response, usedMetric, query, }, setQuery} = this.context + + return ( + <Grid container spacing={24}> + <Grid item xs={12}> + <UploadsHistogram + classes={{root: classes.root}} + height={250} + defaultScale={1} + data={response.statistics.date_histogram} + metric={usedMetric} + metricsDefinitions={metricsDefinitions} + interval={'1M'} + onChanged={(from_time, until_time, interval) => setQuery({...query, from_time: from_time, until_time: until_time, interval: interval})} + {...props} /> + </Grid> + <Grid item xs={12}> + <UploadersList /> + </Grid> + </Grid> + ) + } +} + +export default compose(withApi(false, false), withStyles(UploadsChart.styles))(UploadsChart) \ No newline at end of file diff --git a/nomad/app/api/repo.py b/nomad/app/api/repo.py index 0cbc3847d9..07ad4b07ee 100644 --- a/nomad/app/api/repo.py +++ b/nomad/app/api/repo.py @@ -79,6 +79,8 @@ add_scroll_parameters(_search_request_parser) add_search_parameters(_search_request_parser) _search_request_parser.add_argument( 'date_histogram', type=bool, help='Add an additional aggregation over the upload time') +_search_request_parser.add_argument( + 'interval', type=str, help='Interval to use for upload time aggregation.') _search_request_parser.add_argument( 'metrics', type=str, action='append', help=( 'Metrics to aggregate over all quantities and their values as comma separated list. ' @@ -168,6 +170,7 @@ class RepoCalcsResource(Resource): order_by = args.get('order_by', 'upload_time') date_histogram = args.get('date_histogram', False) + interval = args.get('interval', '1M') metrics: List[str] = request.args.getlist('metrics') with_statistics = args.get('statistics', False) or \ @@ -178,7 +181,7 @@ class RepoCalcsResource(Resource): search_request = search.SearchRequest() apply_search_parameters(search_request, args) if date_histogram: - search_request.date_histogram() + search_request.date_histogram(interval=interval) try: assert page >= 1 diff --git a/nomad/datamodel/datamodel.py b/nomad/datamodel/datamodel.py index fc072d40ed..23c16ca618 100644 --- a/nomad/datamodel/datamodel.py +++ b/nomad/datamodel/datamodel.py @@ -352,7 +352,7 @@ class EntryMetadata(metainfo.MSection): Search( description='Search uploader with exact names.', metric_name='uploaders', metric='cardinality', - many_or='append', search_field='uploader.name.keyword'), + many_or='append', search_field='uploader.name.keyword', default_statistic=True, statistic_size=10), Search( name='uploader_id', search_field='uploader.user_id') ]) diff --git a/nomad/datamodel/dft.py b/nomad/datamodel/dft.py index e4d6d89200..b963ec0bae 100644 --- a/nomad/datamodel/dft.py +++ b/nomad/datamodel/dft.py @@ -45,6 +45,58 @@ basis_sets = { 'planewaves': 'plane waves' } +compound_types = [ + 'unary', + 'binary', + 'ternary', + 'quaternary', + 'quinary', + 'sexinary', + 'septenary', + 'octanary', + 'nonary', + 'decinary' +] + +_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' +] + version_re = re.compile(r'(\d+(\.\d+(\.\d+)?)?)') @@ -60,6 +112,12 @@ def map_basis_set_to_basis_set_label(name): return basis_sets.get(key, name) +def map_atoms_to_compound_type(atoms): + if len(atoms) > len(compound_types): + return '>decinary' + return compound_types[len(atoms) - 1] + + def simplify_version(version): match = version_re.search(version) if match is None: @@ -107,6 +165,12 @@ class DFTMetadata(MSection): description='The system type of the simulated system.', a_search=Search(default_statistic=True)) + compound_type = Quantity( + type=str, default='not processed', + description='The compound type of the simulated system.', + a_search=Search(statistic_size=11, default_statistic=True) + ) + crystal_system = Quantity( type=str, default='not processed', description='The crystal system type of the simulated system.', @@ -155,6 +219,36 @@ class DFTMetadata(MSection): a_search=Search( metric_name='distinct_quantities', metric='cardinality', many_and='append')) + quantities_energy = Quantity( + type=str, shape=['0..*'], + description='Energy-related quantities.', + a_search=Search(many_and='append', default_statistic=True)) + + quantities_electronic = Quantity( + type=str, shape=['0..*'], + description='Electronic structure-related quantities.', + a_search=Search(many_and='append', default_statistic=True)) + + quantities_forces = Quantity( + type=str, shape=['0..*'], + description='Forces-related quantities.', + a_search=Search(many_and='append', default_statistic=True)) + + quantities_vibrational = Quantity( + type=str, shape=['0..*'], + description='Vibrational-related quantities.', + a_search=Search(many_and='append', default_statistic=True)) + + quantities_magnetic = Quantity( + type=str, shape=['0..*'], + description='Magnetic-related quantities.', + a_search=Search(many_and='append', default_statistic=True)) + + quantities_optical = Quantity( + type=str, shape=['0..*'], + description='Optical-related quantities.', + a_search=Search(many_and='append', default_statistic=True)) + geometries = Quantity( type=str, shape=['0..*'], description='Hashes for each simulated geometry', @@ -170,6 +264,16 @@ class DFTMetadata(MSection): description='The labels taken from AFLOW prototypes and springer.', a_search='labels') + labels_springer_compound_class = Quantity( + type=str, shape=['0..*'], + description='Springer compund classification.', + a_search=Search(many_and='append', default_statistic=True, statistic_size=15)) + + labels_springer_classification = Quantity( + type=str, shape=['0..*'], + description='Springer classification by property.', + a_search=Search(many_and='append', default_statistic=True, statistic_size=15)) + optimade = SubSection( sub_section=OptimadeEntry, description='Metadata used for the optimade API.', @@ -213,6 +317,7 @@ class DFTMetadata(MSection): atoms = list(set(normalized_atom_labels(set(atoms)))) atoms.sort() entry.atoms = atoms + self.compound_type = map_atoms_to_compound_type(atoms) self.crystal_system = get_optional_backend_value( backend, 'crystal_system', 'section_symmetry', logger=logger) @@ -243,6 +348,13 @@ class DFTMetadata(MSection): # metrics and quantities quantities = set() geometries = set() + quantities_energy = set() + quantities_electronic = set() + quantities_forces = set() + quantities_vibrational = set() + quantities_magnetic = set() + quantities_optical = set() + n_quantities = 0 n_calculations = 0 n_total_energies = 0 @@ -250,6 +362,18 @@ class DFTMetadata(MSection): for meta_info, event, value in backend.traverse(): quantities.add(meta_info) + if meta_info in _energy_quantities: + quantities_energy.add(meta_info) + elif meta_info in _electronic_quantities: + quantities_electronic.add(meta_info) + elif meta_info in _forces_quantities: + quantities_forces.add(meta_info) + elif meta_info in _vibrational_quantities: + quantities_vibrational.add(meta_info) + elif meta_info in _magnetic_quantities: + quantities_magnetic.add(meta_info) + elif meta_info in _optical_quantities: + quantities_optical.add(meta_info) if event == ParserEvent.add_value or event == ParserEvent.add_array_value: n_quantities += 1 @@ -269,6 +393,12 @@ class DFTMetadata(MSection): self.quantities = list(quantities) self.geometries = list(geometries) + self.quantities_energy = list(quantities_energy) + self.quantities_electronic = list(quantities_electronic) + self.quantities_forces = list(quantities_forces) + self.quantities_vibrational = list(quantities_vibrational) + self.quantities_magnetic = list(quantities_magnetic) + self.quantities_optical = list(quantities_optical) self.n_quantities = n_quantities self.n_calculations = n_calculations self.n_total_energies = n_total_energies @@ -285,6 +415,8 @@ class DFTMetadata(MSection): self.labels.append(Label(label=compound, type='compound_class', source='springer')) for classification in classifications: self.labels.append(Label(label=classification, type='classification', source='springer')) + self.labels_springer_compound_class = list(compounds) + self.labels_springer_classification = list(classifications) aflow_id = get_optional_backend_value(backend, 'prototype_aflow_id', 'section_prototype') aflow_label = get_optional_backend_value(backend, 'prototype_label', 'section_prototype') diff --git a/nomad/search.py b/nomad/search.py index 97d98a8056..b98501c981 100644 --- a/nomad/search.py +++ b/nomad/search.py @@ -337,11 +337,11 @@ class SearchRequest: 'metric:%s' % metric_quantity.metric_name, A(metric_quantity.metric, field=field)) - def date_histogram(self, metrics_to_use: List[str] = []): + def date_histogram(self, metrics_to_use: List[str] = [], interval: str = '1M'): ''' Adds a date histogram on the given metrics to the statistics part. ''' - histogram = A('date_histogram', field='upload_time', interval='1M', format='yyyy-MM-dd') + histogram = A('date_histogram', field='upload_time', interval=interval, format='yyyy-MM-dd') self._add_metrics(self._search.aggs.bucket('statistics:date_histogram', histogram), metrics_to_use) return self -- GitLab