arthurpsm...@gmail.com has uploaded a new change for review. https://gerrit.wikimedia.org/r/245591
Change subject: Added a wikidata-based "chart of the nuclides" under the periodic table app (link to /nuclides) ...................................................................... Added a wikidata-based "chart of the nuclides" under the periodic table app (link to /nuclides) Change-Id: I0a3fe4c20f6442f951cd54a1024334283dd9aaeb --- M app.py A nuclides.py A static/nuclides.css A templates/nuclides.html A units.py 5 files changed, 386 insertions(+), 1 deletion(-) git pull ssh://gerrit.wikimedia.org:29418/labs/tools/ptable refs/changes/91/245591/1 diff --git a/app.py b/app.py index 00ad692..a22ff5c 100644 --- a/app.py +++ b/app.py @@ -20,6 +20,7 @@ from flask.json import JSONEncoder import chemistry +import nuclides class CustomJSONEncoder(JSONEncoder): @@ -34,11 +35,14 @@ # May be set to chemistry.ApiElementProvider (slower, but more up-to-date) element_provider_class = chemistry.WdqElementProvider +nuclide_provider_class = nuclides.WdqNuclideProvider -fake_globals = {'isinstance': isinstance} +fake_globals = {'isinstance': isinstance, 'sorted': sorted} for key in ('EmptyCell', 'UnknownCell', 'ElementCell', 'IndicatorCell'): fake_globals[key] = getattr(chemistry, key) +for key in ('NoneCell', 'NuclideCell'): + fake_globals[key] = getattr(nuclides, key) @app.before_request def set_language(): @@ -49,6 +53,7 @@ if not app.language: app.language = 'en' app.element_provider = element_provider_class(app.language) + app.nuclide_provider = nuclide_provider_class(app.language) @app.route('/') @@ -59,6 +64,14 @@ incomplete=incomplete, **fake_globals) +@app.route('/nuclides') +def nuclides(): + """Render the chart of the nuclides.""" + nuclides, table, incomplete = app.nuclide_provider.get_table() + return render_template('nuclides.html', table=table, + incomplete=incomplete, **fake_globals) + + @app.route('/license') def license(): """Render the license page.""" diff --git a/nuclides.py b/nuclides.py new file mode 100644 index 0000000..9f13c35 --- /dev/null +++ b/nuclides.py @@ -0,0 +1,215 @@ +import json +import operator +from units import time_quantity_in_seconds +from urllib import urlencode +from urllib2 import urlopen +from collections import defaultdict + +try: + from functools import ttl_cache +except ImportError: + from cachetools import ttl_cache + + +# cachetools does not support maxsize=None +@ttl_cache(maxsize=20, ttl=21600) +def get_json_cached(url, data): + """The information is cached for 6 hours.""" + return json.load(urlopen(url, data)) + + +def get_json(url, data): + return get_json_cached(url, urlencode(data)) + + +class NuclideProvider(object): + """Base class for nuclide providers.""" + + WD_API = 'http://www.wikidata.org/w/api.php' + API_LIMIT = 50 + WDQ_API = 'http://wdq.wmflabs.org/api' + + def __init__(self, language): + self.language = language + + @classmethod + def get_entities(cls, ids, **kwargs): + entities = {} + query = dict(action='wbgetentities', format='json', **kwargs) + for index in range(0, len(ids), cls.API_LIMIT): + query['ids'] = '|'.join(ids[index:index + cls.API_LIMIT]) + new_entities = get_json(cls.WD_API, query).get('entities', {}) + entities.update(new_entities) + return entities + + def iter_good(self): + iterator = iter(self) + while True: + try: + yield next(iterator) + except StopIteration: + raise + + def get_table(self): + table = {} + nuclides = [] + incomplete = [] + lastanum = -1 + lastnnum = -1 + for nuclide in self.iter_good(): + if nuclide.atomic_number is not None and nuclide.neutron_number is not None: + if nuclide.atomic_number > lastanum: + lastanum = nuclide.atomic_number + if nuclide.neutron_number > lastnnum: + lastnnum = nuclide.neutron_number + if nuclide.atomic_number not in table: + table[nuclide.atomic_number] = {} + table[nuclide.atomic_number][nuclide.neutron_number] = nuclide + nuclides.append(nuclide) + else: + incomplete.append(nuclide) + nuclides.sort(key=operator.attrgetter('atomic_number', 'neutron_number')) + for anum in range(0, lastanum+1): + if anum not in table: + table[anum] = {} + for nnum in range(0,lastnnum+1): + if nnum in table[anum]: + table[anum][nnum].__class__ = NuclideCell + else: + table[anum][nnum] = NoneCell() + + return nuclides, table, incomplete + + +class WdqNuclideProvider(NuclideProvider): + """Load nuclides from Wikidata Query.""" + def __iter__(self): + wdq = self.get_wdq() + ids = ['Q%d' % item_id for item_id in wdq['items']] + entities = self.get_entities(ids, props='labels|claims', + languages=self.language, languagefallback=1) + nuclides = defaultdict(Nuclide) + wdq['props'] = defaultdict(list, wdq.get('props', {})) + for item_id, datatype, value in wdq['props'][str(Nuclide.atomic_number_pid)]: + if datatype != 'quantity': + continue + value = value.split('|') + if len(value) == 4: + value = map(float, value) + if len(set(value[:3])) == 1 and value[3] == 1 and value[0] == int(value[0]): + nuclides[item_id].atomic_number = int(value[0]) + for item_id, datatype, value in wdq['props'][str(Nuclide.neutron_number_pid)]: + if datatype != 'quantity': + continue + value = value.split('|') + if len(value) == 4: + value = map(float, value) + if len(set(value[:3])) == 1 and value[3] == 1 and value[0] == int(value[0]): + nuclides[item_id].neutron_number = int(value[0]) + for item_id, datatype, value in wdq['props'][str(Nuclide.decay_mode_pid)]: + if datatype != 'item': + continue + nuclides[item_id].decay_modes.append(value) + for item_id, nuclide in nuclides.items(): + nuclide.item_id = 'Q%d' % item_id + for prop in ('atomic_number', 'neutron_number'): + if not hasattr(nuclide, prop): + setattr(nuclide, prop, None) +# ?? nuclide.load_data_from_superclasses(subclass_of[item_id]) + label = None + entity = entities.get(nuclide.item_id) + if entity and 'labels' in entity and len(entity['labels']) == 1: + label = entity['labels'].values()[0]['value'] + nuclide.label = label + + if entity: + claims = entity['claims'] + instance_prop = 'P%d' % Nuclide.instance_pid + if instance_prop in claims: + instance_claims = claims[instance_prop] + for instance_claim in instance_claims: + class_id = instance_claim['mainsnak']['datavalue']['value']['numeric-id'] + if class_id == Nuclide.stable_qid: + nuclide.classes.append("stable") + + half_life = None; + if entity: + claims = entity['claims'] + hlprop = 'P%d' % Nuclide.half_life_pid + if hlprop in claims: + hl_claims = claims[hlprop] + for hl_claim in hl_claims: + half_life = time_quantity_in_seconds(hl_claim) + nuclide.half_life = half_life + if half_life is not None: + if (half_life >= 1.0e9): + nuclide.classes.append("hl1e9") + elif (half_life >= 1.0e6): + nuclide.classes.append("hl1e6") + elif (half_life >= 1.0e3): + nuclide.classes.append("hl1e3") + elif (half_life >= 1.0): + nuclide.classes.append("hl1e0") + elif (half_life >= 1.0e-3): + nuclide.classes.append("hl1e-3") + elif (half_life >= 1.0e-6): + nuclide.classes.append("hl1e-6") + elif (half_life >= 1.0e-9): + nuclide.classes.append("hl1e9") + + yield nuclide + + @classmethod + def get_wdq(cls): + pids = [str(getattr(Nuclide, name)) + for name in ('atomic_number_pid', 'neutron_number_pid', 'decay_mode_pid')] + query = { + 'q': 'claim[%d:(tree[%d][][%d])] AND noclaim[%d:%d]' % (Nuclide.instance_pid, Nuclide.isotope_qid, Nuclide.subclass_pid, Nuclide.instance_pid, Nuclide.isomer_qid), + 'props': ','.join(pids) + } + return get_json(cls.WDQ_API, query) + + +class PropertyAlreadySetException(Exception): + """Property already set.""" + + +class Nuclide(object): + + props = ('atomic_number', 'neutron_number', 'item_id', 'label', 'half_life', 'decay_modes') + atomic_number_pid = 1086 + neutron_number_pid = 1148 + half_life_pid = 2114 + decay_mode_pid = 817 + instance_pid = 31 + subclass_pid = 279 + isotope_qid = 25276 # top-level class under which all isotopes to be found + stable_qid = 878130 # id for stable isotope + isomer_qid = 846110 # metastable isomers all instances of this + + def __init__(self, **kwargs): + self.decay_modes = [] + for key, val in kwargs.items(): + if key in self.props: + setattr(self, key, val) + self.classes = [] + + def __setattr__(self, key, value): + if (key in self.props and hasattr(self, key) and + getattr(self, key) is not None and getattr(self, key) != value): + raise PropertyAlreadySetException + super(Nuclide, self).__setattr__(key, value) + + def __iter__(self): + for key in self.props: + yield (key, getattr(self, key)) + + +class TableCell(object): + """A table cell.""" + +class NuclideCell(Nuclide, TableCell): + """A nuclide cell.""" + +class NoneCell(TableCell): + """An empty cell.""" diff --git a/static/nuclides.css b/static/nuclides.css new file mode 100644 index 0000000..065c856 --- /dev/null +++ b/static/nuclides.css @@ -0,0 +1,60 @@ +table#nuclides{ + border-spacing: 0; +} +table#nuclides td { + width: 5px; + height: 5px; + padding: 0; + font-size: 5px; + text-align: center; + overflow: hidden; + text-overflow: ellipsis; + border: 0px; +} +table#nuclides td.empty { + background: none; +} + +table#nuclides td.nuclide{ + background: #e8e8e8; +} + +table#nuclides td.nuclide.stable { + background: #000; +} + +table#nuclides td.nuclide.hl1e9 { + background: #008; +} + +table#nuclides td.nuclide.hl1e6 { + background: #00c; +} + +table#nuclides td.nuclide.hl1e3 { + background: #00f; +} + +table#nuclides td.nuclide.hl1e0 { + background: #04c; +} + +table#nuclides td.nuclide.hl1e-3 { + background: #088; +} + +table#nuclides td.nuclide.hl1e-6 { + background: #4c4; +} + +table#nuclides td.nuclide.hl1e-9 { + background: #8f8; +} + +table#nuclides td.x_axis { + font-size: 5px; + transform: rotate(90.0deg); +} +table#nuclides td.y_axis { + font-size: 5px; +} diff --git a/templates/nuclides.html b/templates/nuclides.html new file mode 100644 index 0000000..90f5379 --- /dev/null +++ b/templates/nuclides.html @@ -0,0 +1,69 @@ +{% extends "base.html" %} +{%- macro wd_url(nuclide) -%} + //www.wikidata.org/wiki/{{ nuclide.item_id }} +{%- endmacro -%} +{%- macro format_cell(cell) -%} + {%- if isinstance(cell, NoneCell) -%} + <td class="empty"></td> + {%- elif isinstance(cell, NuclideCell) -%} + <td title="{{ cell.label }}" class="nuclide {% for class in cell.classes %} {{ class }}{% endfor %}"> <a href="{{ wd_url(cell) }}"> </a> </td> + {%- else -%} + <td></td> + {%- endif -%} +{%- endmacro -%} +{% block head -%} +<link rel="stylesheet" type="text/css" href="static/nuclides.css"> +{% endblock %} +{% block title %} +Wikidata chart of the nuclides - Wikimedia Tool Labs +{% endblock %} +{% block content %} +<h1>Wikidata chart of the nuclides</h1> +{% if incomplete %} + <h2>Incomplete</h2> + <table> + <thead> + <tr> + <th>nuclide</th> + <th>atomic number</th> + <th>neutron number</th> + </tr> + </thead> + <tbody> + {%- for nuclide in incomplete -%} + <tr> + <td><a href="{{ wd_url(nuclide) }}">{{ nuclide.label }} ({{ nuclide.item_id }})</a></td> + <td>{{ nuclide.atomic_number or '?' }}</td> + <td>{{ nuclide.neutron_number or '?' }}</td> + </tr> + {%- endfor -%} + </tbody> + </ul> +{% endif %} +<table id="nuclides"> + <tr><td>Proton number</td></tr> + {%- for anum in sorted(table.keys(),reverse=True) -%} + <tr> + <td class="y_axis"> + {%- if anum % 5 == 0 -%} + {{ anum }} + {%- endif %} + </td> + {%- for nnum in sorted(table[anum].keys()) -%} + {{ format_cell(table[anum][nnum]) }} + {%- endfor -%} + </tr> + {%- endfor -%} + <tr> + <td> </td> + {%- for nnum in sorted(table[0].keys()) -%} + <td class="x_axis"> + {%- if nnum % 5 == 0 -%} + {{ nnum }} + {%- endif %} + </td> + {%- endfor -%} + </tr> + <tr><td colspan="100" align="center">Neutron number</td></tr> +</table> +{% endblock %} diff --git a/units.py b/units.py new file mode 100644 index 0000000..7e355df --- /dev/null +++ b/units.py @@ -0,0 +1,28 @@ +# time units and their values in multiples of seconds +time_units= { + 11574: 1.0, # second + 7727: 60.0, # minute + 25235: 3600.0, # hour + 573: 86400.0, # day + 23387: 604800.0, # week + 5151: 2.630e6, # month (average) + 1092296: 3.156e7, # year (annum) + 577: 3.156e7, # year (calendar) + 723733: 1.0e-3, # millisecond + 842015: 1.0e-6, # microsecond + 838801: 1.0e-9, # nanosecond + 3902709: 1.0e-12, # picosecond + 1777507: 1.0e-15, # femtosecond + 2483628: 1.0e-18 # attosecond +} + +def time_quantity_in_seconds(claim): + amount = None + if 'mainsnak' in claim: + if 'datavalue' in claim['mainsnak']: + quantity = claim['mainsnak']['datavalue']['value'] + amount = float(quantity['amount']) + unit_uri = quantity['unit'] # Has form http://www.wikidata.org/entity/Q577 + unit_id = int(unit_uri.split('/')[-1].replace('Q','')) + amount *= time_units[unit_id] + return amount -- To view, visit https://gerrit.wikimedia.org/r/245591 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I0a3fe4c20f6442f951cd54a1024334283dd9aaeb Gerrit-PatchSet: 1 Gerrit-Project: labs/tools/ptable Gerrit-Branch: master Gerrit-Owner: arthurpsm...@gmail.com _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits