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

Implemented exclusive atom search. Completes #185.

parent 3eef900c
Pipeline #52884 passed with stages
in 21 minutes and 40 seconds
......@@ -23,12 +23,39 @@ class DFTSearchAggregations extends React.Component {
}
})
constructor(props) {
super(props)
this.handleExclusiveChanged = this.handleExclusiveChanged.bind(this)
}
state = {
exclusive: false
}
handleExclusiveChanged() {
const { searchValues } = this.props
const value = !this.state.exclusive
this.setState({exclusive: value})
if (value) {
searchValues.only_atoms = searchValues.only_atoms || searchValues.atoms
delete searchValues.atoms
} else {
searchValues.atoms = searchValues.only_atoms || searchValues.atoms
delete searchValues.only_atoms
}
this.props.onChange({searchValues: searchValues})
}
handleAtomsChanged(atoms) {
if (this.state.exclusive) {
this.setState({exclusive: false})
}
const searchValues = {...this.props.searchValues}
searchValues.atoms = atoms
if (searchValues.atoms.length === 0) {
delete searchValues.atoms
}
delete searchValues.only_atoms
this.props.onChange({searchValues: searchValues})
}
......@@ -57,8 +84,10 @@ class DFTSearchAggregations extends React.Component {
<CardContent>
<PeriodicTable
aggregations={quantities.atoms} metric={metric}
values={searchValues.atoms || []}
exclusive={this.state.exclusive}
values={searchValues.atoms || searchValues.only_atoms || []}
onChanged={(selection) => this.handleAtomsChanged(selection)}
onExclusiveChanged={this.handleExclusiveChanged}
/>
</CardContent>
</Card>
......
import React from 'react'
import PropTypes from 'prop-types'
import periodicTableData from './PeriodicTableData'
import { withStyles, Typography, Button, Tooltip } from '@material-ui/core'
import { withStyles, Typography, Button, Tooltip, FormControlLabel, Checkbox } from '@material-ui/core'
import chroma from 'chroma-js'
const elements = []
......@@ -112,12 +112,15 @@ class PeriodicTable extends React.Component {
aggregations: PropTypes.object,
metric: PropTypes.string.isRequired,
values: PropTypes.array.isRequired,
onChanged: PropTypes.func.isRequired
onChanged: PropTypes.func.isRequired,
exclusive: PropTypes.bool,
onExclusiveChanged: PropTypes.func.isRequired
}
static styles = theme => ({
root: {
overflowX: 'scroll'
overflowX: 'scroll',
position: 'relative'
},
table: {
margin: 'auto',
......@@ -126,6 +129,12 @@ class PeriodicTable extends React.Component {
maxWidth: 900,
tableLayout: 'fixed',
borderSpacing: theme.spacing.unit * 0.5
},
formContainer: {
position: 'absolute',
top: theme.spacing.unit * 0,
left: '10%',
textAlign: 'center'
}
})
......@@ -151,7 +160,7 @@ class PeriodicTable extends React.Component {
}
render() {
const {classes, aggregations, metric, values} = this.props
const {classes, aggregations, metric, values, exclusive, onExclusiveChanged} = this.props
const max = aggregations ? Math.max(...this.unSelectedAggregations()) || 1 : 1
const heatmapScale = chroma.scale(['#ffcdd2', '#d50000']).domain([1, max], 10, 'log')
return (
......@@ -177,6 +186,17 @@ class PeriodicTable extends React.Component {
))}
</tbody>
</table>
<div className={classes.formContainer}>
<Tooltip title={
'Search for entries with compositions that only (exclusively) contain the ' +
'selected atoms. The default is to return all entries that have at least ' +
'(inclusively) the selected atoms.'}>
<FormControlLabel
control={<Checkbox checked={exclusive} onChange={onExclusiveChanged} />}
label={'only composition that exclusively contain these atoms'}
/>
</Tooltip>
</div>
</div>
)
}
......
......@@ -263,8 +263,8 @@ class RepoCalcsResource(Resource):
return results, 200
except search.ScrollIdNotFound:
abort(400, 'The given scroll_id does not exist.')
# except KeyError as e:
# abort(400, str(e))
except KeyError as e:
abort(400, str(e))
repo_quantity_values_model = api.model('RepoQuantityValues', {
......
......@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Iterable, List, Dict, Type, Tuple
from typing import Iterable, List, Dict, Type, Tuple, Callable, Any
import datetime
from elasticsearch_dsl import Keyword
......@@ -173,15 +173,19 @@ class DomainQuantity:
elastic_mapping: An optional elasticsearch_dsl mapping. Default is ``Keyword``.
elastic_search_type: An optional elasticsearch search type. Default is ``term``.
elastic_field: An optional elasticsearch key. Default is the name of the quantity.
elastic_value: A collable that takes a :class:`CalcWithMetadata` as input and produces the
value for the elastic search index.
"""
def __init__(
self, description: str = None, multi: bool = False, aggregations: int = 0,
order_default: bool = False, metric: Tuple[str, str] = None,
zero_aggs: bool = True, elastic_mapping: str = None,
elastic_search_type: str = 'term', elastic_field: str = None):
zero_aggs: bool = True, metadata_field: str = None,
elastic_mapping: str = None,
elastic_search_type: str = 'term', elastic_field: str = None,
elastic_value: Callable[[Any], Any] = None):
self.name: str = None
self._name: str = None
self.description = description
self.multi = multi
self.order_default = order_default
......@@ -190,14 +194,27 @@ class DomainQuantity:
self.zero_aggs = zero_aggs
self.elastic_mapping = elastic_mapping
self.elastic_search_type = elastic_search_type
self._elastic_key = elastic_field
self.metadata_field = metadata_field
self.elastic_field = elastic_field
self.elastic_value = elastic_value
if self.elastic_value is None:
self.elastic_value = lambda o: o
if self.elastic_mapping is None:
self.elastic_mapping = Keyword(multi=self.multi)
@property
def elastic_field(self) -> str:
return self._elastic_key if self._elastic_key is not None else self.name
def name(self) -> str:
return self._name
@name.setter
def name(self, name: str) -> None:
self._name = name
if self.metadata_field is None:
self.metadata_field = name
if self.elastic_field is None:
self.elastic_field = name
class Domain:
......@@ -274,30 +291,36 @@ class Domain:
reference_domain_calc = domain_entry_class()
reference_general_calc = CalcWithMetadata()
for quantity_name, value in reference_domain_calc.__dict__.items():
# add non specified quantities from additional metadata class fields
for quantity_name in reference_domain_calc.__dict__.keys():
if not hasattr(reference_general_calc, quantity_name):
quantity = quantities.get(quantity_name, None)
if quantity is None:
quantity = DomainQuantity()
quantities[quantity_name] = quantity
quantities[quantity_name] = DomainQuantity()
quantity.name = quantity_name
quantity.multi = isinstance(value, list)
self.quantities[quantity.name] = quantity
# add all domain quantities
for quantity_name, quantity in quantities.items():
quantity.name = quantity_name
self.quantities[quantity.name] = quantity
for quantity_name in quantities.keys():
assert hasattr(reference_domain_calc, quantity_name) and not hasattr(reference_general_calc, quantity_name), \
'quantity does not exist or overrides general non domain quantity'
# update the multi status from an example value
if quantity.metadata_field in reference_domain_calc.__dict__:
quantity.multi = isinstance(
reference_domain_calc.__dict__[quantity.metadata_field], list)
assert any(quantity.order_default for quantity in Domain.instances[name].quantities.values()), \
'you need to define a order default quantity'
assert not hasattr(reference_general_calc, quantity_name), \
'quantity overrides general non domain quantity'
# construct search quantities from base and domain quantities
self.search_quantities = dict(**Domain.base_quantities)
for name, quantity in self.search_quantities.items():
quantity.name = name
for quantity_name, quantity in self.search_quantities.items():
quantity.name = quantity_name
self.search_quantities.update(self.quantities)
assert any(quantity.order_default for quantity in Domain.instances[name].quantities.values()), \
'you need to define a order default quantity'
@property
def metrics(self) -> Dict[str, Tuple[str, str]]:
"""
......
......@@ -156,6 +156,12 @@ class DFTCalcWithMetadata(CalcWithMetadata):
self.n_geometries = n_geometries
def only_atoms(atoms):
numbers = [ase.data.atomic_numbers[atom] for atom in atoms]
only_atoms = [ase.data.chemical_symbols[number] for number in sorted(numbers)]
return ''.join(only_atoms)
Domain('DFT', DFTCalcWithMetadata, quantities=dict(
formula=DomainQuantity(
'The chemical (hill) formula of the simulated system.',
......@@ -163,6 +169,10 @@ Domain('DFT', DFTCalcWithMetadata, quantities=dict(
atoms=DomainQuantity(
'The atom labels of all atoms in the simulated system.',
aggregations=len(ase.data.chemical_symbols), multi=True, zero_aggs=False),
only_atoms=DomainQuantity(
'The atom labels concatenated in species-number order. Used with keyword search '
'to facilitate exclusive searches.',
elastic_value=only_atoms, metadata_field='atoms', multi=True),
basis_set=DomainQuantity(
'The used basis set functions.', aggregations=10),
xc_functional=DomainQuantity(
......
......@@ -149,8 +149,10 @@ class Entry(Document, metaclass=WithDomain):
self.references = [ref.value for ref in source.references]
self.datasets = [Dataset.from_dataset_popo(ds) for ds in source.datasets]
for quantity in datamodel.Domain.instance.quantities.keys():
setattr(self, quantity, getattr(source, quantity))
for quantity in datamodel.Domain.instance.quantities.values():
setattr(
self, quantity.name,
quantity.elastic_value(getattr(source, quantity.metadata_field)))
def delete_upload(upload_id):
......@@ -239,6 +241,8 @@ def _construct_search(
else:
raise KeyError('Unknown quantity %s' % key)
value = quantity.elastic_value(value)
if isinstance(value, list):
values = value
else:
......
......@@ -748,6 +748,10 @@ class TestRepo():
(1, 'atoms', 'Br'),
(1, 'atoms', 'Fe'),
(0, 'atoms', ['Fe', 'Br']),
(0, 'only_atoms', ['Br', 'Si']),
(1, 'only_atoms', ['Fe']),
(1, 'only_atoms', ['Br', 'K', 'Si']),
(1, 'only_atoms', ['Br', 'Si', 'K']),
(1, 'comment', 'specific'),
(1, 'authors', 'Hofstadter, Leonard'),
(2, 'files', 'test/mainfile.txt'),
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment