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) 
}}">&nbsp;</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

Reply via email to