import datetime
import json
import os
import pathlib
import ssl
import warnings
from urllib import parse, request
from urllib.error import URLError
from . import DB_FILE, SLDDB
from .dbconfig import WEBAPI_URL
from .element_table import get_element
from .material import Formula, Material
[docs]
class SLD_API:
"""
Python API for users of the SLDDB data.
Allows to query the online database for materials, calculate SLDs and add new materials.
If connection to the server fails, a local copy of the database is used, instead.
Usage:
from orsopy.slddb import api
res=api.search(formula="Fe2O3")
res[0]['density'] => ....
m=api.material(res[0]['ID']) # retrieve all data for the given material, see Material class.
sldn=m.rho_n # get nuclear neutron SLD (complex number)
sldm=m.rho_m # get magnetic neutron SLD (real number)
sldx=m.f_of_E(E=8.047823) # get x-ray SLD (complex number) for given energy, default is Cu-Kalpha
# custom material just for SLD calculation, requires either dens, fu_volume, rho_n or xsld+xE
m=api.custom(formula='Au', dens=19.3)
Units of results/queries:
density: g/cm³
roh_n: Å^{-2}
roh_m: Å^{-2}
sldx: Å^{-2}
fu_volume: ų
"""
db_suburl = "download_db"
max_age = 1
db: SLDDB = None
def __init__(self):
self.first_access = True
self.use_webquery = True # only try webquery once, if error occurs switch to local database
[docs]
def check(self):
# make sure the local database file is up to date, if not try to download newest version
if self.first_access:
now = datetime.datetime.now()
try:
stat = pathlib.Path(DB_FILE).stat()
except FileNotFoundError:
self.download_db()
else:
mtime = datetime.datetime.fromtimestamp(stat.st_ctime)
try:
mtime = max(mtime, datetime.datetime.fromtimestamp(stat.st_mtime))
except AttributeError:
pass
if (now - mtime).days > self.max_age:
try:
self.download_db()
except URLError as err:
warnings.warn("Can't download new version of database; " + str(err))
self.db = SLDDB(DB_FILE) # after potential update, make connection with local database
self.first_access = False
else:
return
[docs]
def download_db(self):
# noinspection PyUnresolvedReferences
context = ssl._create_unverified_context()
res = request.urlopen(WEBAPI_URL + self.db_suburl, context=context)
data = res.read()
if not data.startswith(b"SQLite format 3"):
raise ValueError("Error when downloading new database")
if os.path.isfile(DB_FILE):
os.remove(DB_FILE)
with open(DB_FILE, "wb") as fh:
fh.write(data)
[docs]
@staticmethod
def webquery(qdict):
data = parse.urlencode(qdict)
# noinspection PyUnresolvedReferences
context = ssl._create_unverified_context()
webdata = request.urlopen(WEBAPI_URL + "api?" + data, context=context)
return json.loads(webdata.read()) # return decoded data
[docs]
def localquery(self, qdict):
return self.db.search_material(**qdict)
[docs]
def localmaterial(self, ID):
res = self.db.search_material(ID=ID)
return self.db.select_material(res[0])
[docs]
def search(self, **opts):
"""
Search for a particular material using a combination of provided search keys.
Examples:
api.search(formula="Fe2O3")
api.search(density=5.242)
api.search(name='iron')
"""
if not self.use_webquery:
return self.localquery(opts)
self.check()
try:
res = self.webquery(opts)
except URLError:
self.use_webquery = False
res = self.localquery(opts)
return res
[docs]
def material(self, ID):
"""
Returns the material object for a certain database entry specified by its unique ID.
Example:
res=api.search(formula='Fe')
material=api.material(res[0]['ID'])
print(material.dens, material.rho_n, material.f_of_E(8.0))
"""
if not self.use_webquery:
return self.localmaterial(ID)
self.check()
try:
res = self.webquery({"ID": int(ID)})
except URLError:
self.use_webquery = False
return self.localmaterial(ID)
else:
f = Formula(res["formula"], sort=False)
mat_data = dict(dens=float(res["density"]), ID=ID, extra_data={})
if res.get("name", None):
mat_data["name"] = res["name"]
if res.get("mu", 0.0):
mat_data["mu"] = res["mu"]
elif res.get("M", 0.0):
mat_data["M"] = res["M"]
for key in ["ORSO_validated", "description", "doi", "reference"]:
if key in res:
mat_data["extra_data"][key] = res[key]
out = Material([(get_element(element), amount) for element, amount in f], **mat_data)
return out
[docs]
@staticmethod
def custom(formula, dens=None, fu_volume=None, rho_n=None, mu=0.0, xsld=None, xE=None):
"""
Returns the material object for a certain material as specified by caller.
Example:
res=api.custom('Fe', dens=7.8)
print(material.dens, material.rho_n, material.f_of_E(8.0))
"""
f = Formula(formula, sort=False)
out = Material(
[(get_element(element), amount) for element, amount in f],
dens=dens,
fu_volume=fu_volume,
rho_n=rho_n,
mu=mu,
xsld=xsld,
xE=xE,
)
return out
[docs]
def bio_blender(self, sequence, molecule="protein"):
"""
Get material for protein, DNA or RNA. Provide a letter sequence and molecule type ('protein', 'dna', 'rna').
"""
opts = {molecule.lower(): sequence, "sldcalc": "true"}
res = self.webquery(opts)
mat_data = dict(fu_volume=float(res["fu_volume"]), name=f"BioBlender-{molecule.lower()}", extra_data={})
for key in [
"description",
]:
if key in res:
mat_data["extra_data"][key] = res[key]
out = Material(Formula(res["formula"]), **mat_data)
return out