1# coding: utf-8 2from __future__ import print_function, division, unicode_literals, absolute_import 3 4import sys 5import os 6import json 7 8from collections import OrderedDict, defaultdict 9from itertools import groupby 10 11# Helper functions (coming from AbiPy) 12class lazy_property(object): 13 """ 14 lazy_property descriptor 15 16 Used as a decorator to create lazy attributes. 17 Lazy attributes are evaluated on first use. 18 """ 19 20 def __init__(self, func): 21 self.__func = func 22 from functools import wraps 23 wraps(self.__func)(self) 24 25 def __get__(self, inst, inst_cls): 26 if inst is None: 27 return self 28 29 if not hasattr(inst, '__dict__'): 30 raise AttributeError("'%s' object has no attribute '__dict__'" 31 % (inst_cls.__name__,)) 32 33 name = self.__name__ 34 if name.startswith('__') and not name.endswith('__'): 35 name = '_%s%s' % (inst_cls.__name__, name) 36 37 value = self.__func(inst) 38 inst.__dict__[name] = value 39 return value 40 41 @classmethod 42 def invalidate(cls, inst, name): 43 """Invalidate a lazy attribute. 44 45 This obviously violates the lazy contract. A subclass of lazy 46 may however have a contract where invalidation is appropriate. 47 """ 48 inst_cls = inst.__class__ 49 50 if not hasattr(inst, '__dict__'): 51 raise AttributeError("'%s' object has no attribute '__dict__'" 52 % (inst_cls.__name__,)) 53 54 if name.startswith('__') and not name.endswith('__'): 55 name = '_%s%s' % (inst_cls.__name__, name) 56 57 if not isinstance(getattr(inst_cls, name), cls): 58 raise AttributeError("'%s.%s' is not a %s attribute" 59 % (inst_cls.__name__, name, cls.__name__)) 60 61 if name in inst.__dict__: 62 del inst.__dict__[name] 63 64 65def is_string(s): 66 """True if s behaves like a string (duck typing test).""" 67 try: 68 s + " " 69 return True 70 except TypeError: 71 return False 72 73 74def list_strings(arg): 75 """ 76 Always return a list of strings, given a string or list of strings as input. 77 78 :Examples: 79 80 >>> list_strings('A single string') 81 ['A single string'] 82 83 >>> list_strings(['A single string in a list']) 84 ['A single string in a list'] 85 86 >>> list_strings(['A','list','of','strings']) 87 ['A', 'list', 'of', 'strings'] 88 """ 89 if is_string(arg): 90 return [arg] 91 else: 92 return arg 93 94def splitall(path): 95 """Return list with all components of a path.""" 96 allparts = [] 97 while True: 98 parts = os.path.split(path) 99 if parts[0] == path: # sentinel for absolute paths 100 allparts.insert(0, parts[0]) 101 break 102 elif parts[1] == path: # sentinel for relative paths 103 allparts.insert(0, parts[1]) 104 break 105 else: 106 path = parts[0] 107 allparts.insert(0, parts[1]) 108 return allparts 109 110 111# Unit names supported in Abinit input. 112ABI_UNITS = [ 113 'au', 114 'Angstr', 115 'Angstrom', 116 'Angstroms', 117 'Bohr', 118 'Bohrs', 119 'eV', 120 'Ha', 121 'Hartree', 122 'Hartrees', 123 'K', 124 'Ry', 125 'Rydberg', 126 'Rydbergs', 127 'T', 128 'Tesla', 129] 130 131# Operators supported by parser 132ABI_OPS = ['sqrt', 'end', '*', '/'] 133 134 135# List of strings with possible character of variables. 136# This is the reference set that will checked against the input 137# given by the developer in the variables_CODENAME modules. 138ABI_CHARACTERISTICS = [ 139 "DEVELOP", 140 "EVOLVING", 141 "ENERGY", 142 "INPUT_ONLY", 143 "INTERNAL_ONLY", 144 "LENGTH", 145 "MAGNETIC_FIELD", 146 "NO_MULTI", 147] 148 149# external parametersare not input variables, 150# but are used in the documentation of other variables. 151ABI_EXTERNAL_PARAMS = OrderedDict([ 152 ("AUTO_FROM_PSP", "Means that the value is read from the PSP file"), 153 ("CUDA", "True if CUDA is enabled (compilation)"), 154 ("ETSF_IO", "True if ETSF_IO is enabled (compilation)"), 155 ("FFTW3", "True if FFTW3 is enabled (compilation)"), 156 ("MPI_IO", "True if MPI_IO is enabled (compilation)"), 157 ("NPROC", "Number of processors used for Abinit"), 158 ("PARALLEL", "True if the code is compiled with MPI"), 159 ("SEQUENTIAL", "True if the code is compiled without MPI"), 160]) 161 162# List of topics 163ABI_TOPICS = [ 164 "Abipy", 165 "APPA", 166 "Artificial", 167 "AtomManipulator", 168 "AtomTypes", 169 "Bader", 170 "Band2eps", 171 "Berry", 172 "BandOcc", 173 "BSE", 174 "ConstrainedPol", 175 "Control", 176 "Coulomb", 177 "CRPA", 178 "crystal", 179 "DFT+U", 180 "DeltaSCF", 181 "DensityPotential", 182 "Dev", 183 "DFPT", 184 "DMFT", 185 "EffMass", 186 "EFG", 187 "Elastic", 188 "ElPhonInt", 189 "ElPhonTransport", 190 "ElecDOS", 191 "ElecBandStructure", 192 "FileFormats", 193 "ForcesStresses", 194 "FrequencyMeshMBPT", 195 "GeoConstraints", 196 "GeoOpt", 197 "Git", 198 "GSintroduction", 199 "GW", 200 "GWls", 201 "Hybrids", 202 "k-points", 203 "LDAminushalf", 204 "LOTF", 205 "MagField", 206 "MagMom", 207 "MolecularDynamics", 208 "multidtset", 209 "nonlinear", 210 "Optic", 211 "Output", 212 "parallelism", 213 "PAW", 214 "PIMD", 215 "Planewaves", 216 "Phonons", 217 "PhononBands", 218 "PhononWidth", 219 "PortabilityNonRegression", 220 "positron", 221 "printing", 222 "PseudosPAW", 223 "q-points", 224 "RandStopPow", 225 "Recursion", 226 "RPACorrEn", 227 "SCFControl", 228 "SCFAlgorithms", 229 "SelfEnergy", 230 "SmartSymm", 231 "spinpolarisation", 232 "STM", 233 "Susceptibility", 234 "TDDFT", 235 "TDepES", 236 "Temperature", 237 "TransPath", 238 "TuningSpeed", 239 "Unfolding", 240 "UnitCell", 241 "vdw", 242 "Verification", 243 "Wannier", 244 "Wavelets", 245 "xc", 246] 247 248# Relevance associated to the topic 249ABI_RELEVANCES = OrderedDict([ 250 ("compulsory", 'Compulsory input variables'), 251 ("basic", 'Basic input variables'), 252 ("useful", 'Useful input variables'), 253 ("internal", 'Relevant internal variables'), 254 ("prpot", 'Printing input variables for potentials'), 255 ("prfermi", 'Printing input variables for fermi level or surfaces'), 256 ("prden", 'Printing input variables for density, eigenenergies, k-points and wavefunctions'), 257 ("prgeo", 'Printing input variables for geometry'), 258 ("prdos", "Printing DOS-related input variables"), 259 ("prgs", 'Printing other ground-state input variables'), 260 ("prngs", 'Printing non-ground-state input variables'), 261 ("prmisc", 'Printing miscellaneous files'), 262 ("expert", 'Input variables for experts'), 263]) 264 265 266class Variable(object): 267 """ 268 This object gathers information about a single variable. name, associated topics, description etc 269 It's constructed from the variables_CODENANE.py modules but client code usually 270 interact with variables via the :class:`VarDatabase` dictionary. 271 """ 272 def __init__(self, 273 abivarname=None, 274 varset=None, 275 vartype=None, 276 topics=None, 277 dimensions=None, 278 defaultval=None, 279 mnemonics=None, 280 characteristics=None, 281 excludes=None, 282 requires=None, 283 commentdefault=None, 284 commentdims=None, 285 added_in_version=None, 286 alternative_name=None, 287 text=None, 288 ): 289 """ 290 Args: 291 abivarname (str): Name of the variable (including @code if not abinit e.g asr@anaddb). 292 Required 293 varset (str): The group this variable belongs to (could be code if code has no group). 294 Required 295 vartype (str): The type of the variable. Required 296 topics (list): List of strings with topics. Required 297 dimensions: List of strings with dimensions or "scalar". Required. 298 defaultval: Default value. None if no default is provided. Other possibilities are ... 299 Either constant number, formula or another variable 300 mnemonics (str): Mnemonic string (required). 301 characteristics (list): List of characteristics or None 302 excludes (str): String with variables that are exluded if this variable is given. 303 requires (str): String with variables that are required. 304 commentdefault=None, 305 commentdims=None, 306 added_in_version (str): String with the Abinit version in which this variable was added. 307 None if variable is present in Abinit <= 8.6.3 308 alternative_name: alias name (used if a new variable with a different name was introduced, in place 309 of of an old variable that is still supported. 310 text: markdown string with documentation. Required. 311 """ 312 self.abivarname = abivarname 313 self.varset = varset 314 self.vartype = vartype 315 self.topics = topics 316 self.dimensions = dimensions 317 self.defaultval = defaultval 318 self.mnemonics = mnemonics 319 self.characteristics = characteristics 320 self.excludes = excludes 321 self.requires = requires 322 self.commentdefault = commentdefault 323 self.commentdims = commentdims 324 self.added_in_version = added_in_version 325 self.alternative_name = alternative_name 326 self.text = my_unicode(text) 327 328 errors = [] 329 for a in ("abivarname", "varset", "vartype", "topics", "dimensions", "text", "added_in_version"): 330 if getattr(self, a) is None: 331 errors.append("attribute %s is mandatory" % a) 332 if errors: 333 raise ValueError("Errors in %s:\n%s" % (self.abivarname, "\n".join(errors))) 334 335 @lazy_property 336 def name(self): 337 """Name of the variable without the executable name.""" 338 return self.abivarname if "@" not in self.abivarname else self.abivarname.split("@")[0] 339 340 @lazy_property 341 def executable(self): 342 """string with the name of the code associated to this variable.""" 343 if "@" in self.abivarname: 344 code = self.abivarname.split("@")[1] 345 assert code == self.varset 346 else: 347 code = "abinit" 348 return code 349 350 @lazy_property 351 def website_url(self): 352 """ 353 The absolute URL associated to this variable on the Abinit website. 354 """ 355 # This is gonna be the official API on the server 356 #docs.abinit.org/vardocs/CODENAME/VARNAME?version=8.6.2 357 #return "https://docs.abinit.org/vardocs/%s/%s" % (self.executable, self.name) 358 359 # For the time being, we have to use: 360 # variables/eph/#asr 361 # variables/anaddb#asr 362 if self.executable == "abinit": 363 return "https://docs.abinit.org/variables/%s#%s" % (self.varset, self.name) 364 else: 365 return "https://docs.abinit.org/variables/%s#%s" % (self.executable, self.name) 366 367 @lazy_property 368 def topic2relevances(self): 369 """topic --> list of tribes""" 370 assert self.topics is not None 371 od = OrderedDict() 372 for tok in self.topics: 373 topic, tribe = [s.strip() for s in tok.split("_")] 374 if topic not in od: od[topic] = [] 375 od[topic].append(tribe) 376 return od 377 378 @lazy_property 379 def is_internal(self): 380 """True if this is an internal variable.""" 381 return self.characteristics is not None and '[[INTERNAL_ONLY]]' in self.characteristics 382 383 @lazy_property 384 def wikilink(self): 385 """Abinit wikilink.""" 386 return "[[%s:%s]]" % (self.executable, self.name) 387 388 def __repr__(self): 389 """Variable name + mnemonics""" 390 return self.abivarname + " <" + str(self.mnemonics) + ">" 391 392 def to_string(self, verbose=0): 393 """String representation with verbosity level `verbose`.""" 394 return "Variable " + str(self.abivarname) + " (default = " + str(self.defaultval) + ")" 395 396 def __str__(self): 397 return self.to_string() 398 399 def __hash__(self): 400 # abivarname is unique 401 return hash(self.abivarname) 402 403 def __eq__(self, other): 404 if other is None: return False 405 return self.abivarname == other.abivarname 406 407 def __ne__(self, other): 408 return not (self == other) 409 410 @lazy_property 411 def info(self): 412 """String with extra info on the variable.""" 413 attrs = [ 414 "vartype", "characteristics", "mnemonics", "dimensions", "defaultval", 415 "abivarname", "commentdefault", "commentdims", "varset", 416 "requires", "excludes", 417 "added_in_version", "alternative_name", 418 ] 419 420 def astr(obj): 421 return str(obj).replace("[[", "").replace("]]", "") 422 423 d = {k: astr(getattr(self, k)) for k in attrs if getattr(self, k) is not None} 424 return json.dumps(d, indent=4, sort_keys=True) 425 426 def _repr_html_(self): 427 """Integration with jupyter notebooks.""" 428 try: 429 import markdown 430 except ImportError: 431 markdown = None 432 433 if markdown is None: 434 html = "<h2>Default value:</h2>" + my_unicode(self.defaultval) + "<br/><h2>Description</h2>" + self.text 435 return html.replace("[[", "<b>").replace("]]", "</b>") 436 else: 437 md = self.text.replace("[[", "<b>").replace("]]", "</b>") 438 return markdown.markdown(""" 439## Default value: 440{defaultval} 441 442## Description: 443{md} 444""".format(defaultval=my_unicode(self.defaultval), md=my_unicode(md))) 445 446 def browse(self): 447 """Open variable documentation in browser.""" 448 import webbrowser 449 return webbrowser.open(self.website_url) 450 451 @lazy_property 452 def isarray(self): 453 """True if this variable is an array.""" 454 return not (is_string(self.dimensions) and self.dimensions == "scalar") 455 456 def depends_on_dimension(self, dimname): 457 """ 458 True if variable is an array whose shape depends on dimension name `dimname`. 459 460 Args: dimname: String of :class:`Variable` object. 461 """ 462 if not self.isarray: return False 463 if isinstance(dimname, Variable): dimname = dimname.name 464 # This test is not very robust and can fail. 465 # Assume no space between `[` and name (there should be a test for this...) 466 key = "[[%s]]" % dimname 467 for d in self.dimensions: 468 if key in str(d): return True 469 return False 470 471 def html_link(self, label=None): 472 """String with the URL of the web page.""" 473 label = self.name if label is None else label 474 return '<a href="%s" target="_blank">%s</a>' % (self.website_url, label) 475 476 def get_parent_names(self): 477 """ 478 Return set of strings with the name of the parents 479 i.e. the variables that are connected to this variable 480 (either because they are present in dimensions on in requires). 481 """ 482 #if hasattr(self, ... 483 import re 484 parent_names = [] 485 WIKILINK_RE = r'\[\[([\w0-9_ -]+)\]\]' 486 # TODO 487 # parent = self[parent] 488 # KeyError: "'nzchempot' 489 #WIKILINK_RE = r'\[\[([^\[]+)\]\]' 490 if isinstance(self.dimensions, (list, tuple)): 491 for dim in self.dimensions: 492 dim = str(dim) 493 m = re.match(WIKILINK_RE, dim) 494 if m: 495 parent_names.append(m.group(1)) 496 497 if self.requires is not None: 498 parent_names.extend([m.group(1) for m in re.finditer(WIKILINK_RE, self.requires) if m]) 499 500 # Convert to set and remove possibile self-reference. 501 parent_names = set(parent_names) 502 parent_names.discard(self.name) 503 return parent_names 504 505 def internal_link(self, website, page_rpath, label=None, cls=None): 506 """String with the website internal URL.""" 507 token = "%s:%s" % (self.executable, self.name) 508 a = website.get_wikilink(token, page_rpath) 509 cls = a.get("class") if cls is None else cls 510 return '<a href="%s" class="%s">%s</a>' % (a.get("href"), cls, a.text if label is None else label) 511 512 @staticmethod 513 def format_dimensions(dimensions): 514 """Pretty print dimensions.""" 515 if dimensions is None: 516 s = '' 517 elif dimensions == "scalar": 518 s = 'scalar' 519 else: 520 #s = str(dimensions) 521 if isinstance(dimensions, (list, tuple)): 522 s = '(' 523 for dim in dimensions: 524 s += str(dim) + ',' 525 s = s[:-1] 526 s += ')' 527 else: 528 s = str(dimensions) 529 530 return s 531 532 def to_abimarkdown(self, with_hr=True): 533 """ 534 Return markdown string Can use Abinit markdown extensions. 535 """ 536 lines = []; app = lines.append 537 538 app("## **%s** \n\n" % self.name) 539 app("*Mnemonics:* %s " % str(self.mnemonics)) 540 if self.characteristics: 541 app("*Characteristics:* %s " % ", ".join(self.characteristics)) 542 if self.topic2relevances: 543 app("*Mentioned in topic(s):* %s " % ", ".join("[[topic:%s]]" % k for k in self.topic2relevances)) 544 app("*Variable type:* %s " % str(self.vartype)) 545 if self.dimensions: 546 app("*Dimensions:* %s " % self.format_dimensions(self.dimensions)) 547 if self.commentdims: 548 app("*Commentdims:* %s " % self.commentdims) 549 app("*Default value:* %s " % self.defaultval) 550 if self.commentdefault: 551 app("*Comment:* %s " % self.commentdefault) 552 if self.requires: 553 app("*Only relevant if:* %s " % str(self.requires)) 554 if self.excludes: 555 app("*The use of this variable forbids the use of:* %s " % self.excludes) 556 557 # Add links to tests. 558 if hasattr(self, "tests") and not self.is_internal: 559 # Constitutes an usage report e.g. 560 # Rarely used, in abinit tests [8/888], in tuto abinit tests [2/136]. 561 assert hasattr(self, "tests_info") 562 tests_info = self.tests_info 563 ratio_all = len(self.tests) / tests_info["num_all_tests"] 564 frequency = "Rarely used" 565 if ratio_all > 0.5: 566 frequency = "Very frequently used" 567 elif ratio_all > 0.01: 568 frequency = "Moderately used" 569 570 info = "%s, [%d/%d] in all %s tests, [%d/%d] in %s tutorials" % ( 571 frequency, len(self.tests), tests_info["num_all_tests"], self.executable, 572 tests_info["num_tests_in_tutorial"], tests_info["num_all_tutorial_tests"], self.executable) 573 574 # Use https://facelessuser.github.io/pymdown-extensions/extensions/details/ 575 # Truncate list of tests if we have more that `max_ntests` entries. 576 count, max_ntests = 0, 20 577 app('\n??? note "Test list (click to open). %s"' % info) 578 tlist = sorted(self.tests, key=lambda t: t.suite_name) 579 d = {} 580 for suite_name, tests_in_suite in groupby(tlist, key=lambda t: t.suite_name): 581 ipaths = [os.path.join(*splitall(t.inp_fname)[-4:]) for t in tests_in_suite] 582 count += len(ipaths) 583 d[suite_name] = ipaths 584 585 for suite_name, ipaths in d.items(): 586 if count > max_ntests: ipaths = ipaths[:min(3, len(ipaths))] 587 s = "- " + suite_name + ": " + ", ".join("[[%s|%s]]" % (p, os.path.basename(p)) for p in ipaths) 588 if count > max_ntests: s += " ..." 589 app(" " + s) 590 app("\n\n") 591 592 # Add text with description. 593 app(2 * "\n") 594 app(self.text) 595 if with_hr: app("* * *" + 2*"\n") 596 597 return "\n".join(lines) 598 599 def validate(self): 600 """Validate variable. Raises ValueError if not valid.""" 601 errors = [] 602 eapp = errors.append 603 604 try: 605 svar = str(self) 606 except Exception as exc: 607 svar = "Unknown" 608 eapp(str(exc)) 609 610 if self.abivarname is None: 611 eapp("Variable `%s` has no name" % svar) 612 613 if self.vartype is None: 614 eapp("Variable `%s` has no vartype" % svar) 615 elif not self.vartype in ("integer", "real", "string"): 616 eapp("%s must have vartype in ['integer', 'real', 'string'].") 617 618 if self.topics is None: 619 eapp("%s does not have at least one topic and the associated relevance" % svar) 620 621 for topic, relevances in self.topic2relevances.items(): 622 if topic not in ABI_TOPICS: 623 eapp("%s delivers topic `%s` that does not belong to the allowed list" % (sname, topic)) 624 for relevance in relevances: 625 if relevance not in ABI_RELEVANCES: 626 eapp("%s delivers relevance `%s` that does not belong to the allowed list" % (sname, relevance)) 627 628 # Compare the characteristics of this variable with the refs to detect possible typos. 629 if self.characteristics is not None: 630 if not isinstance(self.characteristics, list): 631 eapp("The field characteristics of %s is not a list" % svar) 632 else: 633 for cat in self.characteristics: 634 if cat.replace("[[", "").replace("]]", "") not in ABI_CHARACTERISTICS: 635 eapp("The characteristics %s of %s is not valid" % (cat, svar)) 636 637 if self.dimensions is None: 638 eapp("%s does not have a dimension. If it is a *scalar*, it must be declared so." % svar) 639 else: 640 if self.dimensions != "scalar": 641 if not isinstance(self.dimensions, (list, ValueWithConditions)): 642 eapp('The dimensions field of %s is not a list neither a valuewithconditions' % svar) 643 644 if self.varset is None: 645 eapp('`%s` does not have a varset' % svar) 646 #else: 647 # if not isinstance(self.varset, str) or self.varset not in ref_varset: 648 # print('The field varset of %s should be one of the valid varsets' % str(self)) 649 650 if len(self.name) > 20: 651 eapp("Lenght of `%s` is longer than 20 characters." % svar) 652 653 if errors: 654 raise ValueError("\n".join(errors)) 655 656class ValueWithUnit(object): 657 """ 658 This type allows to specify values with units: 659 """ 660 def __init__(self, value=None, units=None): 661 self.value = value 662 self.units = units 663 664 def __str__(self): 665 return str(self.value) + " " + str(self.units) 666 667 def __repr__(self): 668 return str(self) 669 670class Range(object): 671 """ 672 Specifies a range (start:stop:step) 673 """ 674 start = None 675 stop = None 676 677 def __init__(self, start=None, stop=None): 678 self.start = start 679 self.stop = stop 680 681 def isin(self, value): 682 """True if value is in range.""" 683 isin = True 684 if self.start is not None: 685 isin = isin and (self.start <= self.value) 686 if stop is not None: 687 isin = isin and self.stop > self.value 688 return str(self) 689 690 def __repr__(self): 691 # Add whitespace after `[` or before `]` to avoid [[[ and ]]] patterns 692 # that enter into conflict with wikiling syntax [[...]] 693 if self.start is not None and self.stop is not None: 694 return "[ " + str(self.start) + " ... " + str(self.stop) + " ]" 695 if self.start is not None: 696 return "[ " + str(self.start) + "; ->" 697 if self.stop is not None: 698 return "<-;" + str(self.stop) + " ]" 699 else: 700 return None 701 702 703class ValueWithConditions(dict): 704 """ 705 Used for variables whose value depends on a list of conditions. 706 707 .. example: 708 709 ValueWithConditions({'[[paral_kgb]]==1': '6', 'defaultval': 2}), 710 711 Means that the variable is set to 6 if paral_kgb == 1 else 2 712 """ 713 def __repr__(self): 714 s = '' 715 for key in self: 716 if key != 'defaultval': 717 s += str(self[key]) + ' if ' + str(key) + ',\n' 718 s += str(self["defaultval"]) + ' otherwise.\n' 719 return s 720 721 def __str__(self): 722 return self.__repr__() 723 724 725class MultipleValue(object): 726 """ 727 Used for variables that can assume multiple values. 728 This is the equivalent to the X * Y syntax in the Abinit parser. 729 If X is null, it means that you want to do *Y (all Y) 730 """ 731 def __init__(self, number=None, value=None): 732 self.number = number 733 self.value = value 734 735 def __repr__(self): 736 if self.number is None: 737 return "*" + str(self.value) 738 else: 739 return str(self.number) + " * " + str(self.value) 740 741def my_unicode(s): 742 """Convert string to unicode (needed for py2.7 DOH!)""" 743 return unicode(s) if sys.version_info[0] <= 2 else str(s) 744 745############## 746# Public API # 747############## 748 749_VARS = None 750 751def get_codevars(): 752 """ 753 Return the database of variables indexed by code name and cache it. 754 Main entry point for client code. 755 """ 756 global _VARS 757 if _VARS is None: _VARS = VarDatabase.from_pyfiles() 758 return _VARS 759 760 761class VarDatabase(OrderedDict): 762 """ 763 This object stores the full set of input variables for all the Abinit executables. 764 in a dictionary mapping the name of the code to a subdictionary of variables. 765 """ 766 767 all_characteristics = ABI_CHARACTERISTICS 768 all_external_params = ABI_EXTERNAL_PARAMS 769 770 @classmethod 771 def from_pyfiles(cls, dirpath=None): 772 """ 773 Initialize the object from python modules inside dirpath. 774 If dirpath is None, the directory of the present module is used. 775 """ 776 if dirpath is None: 777 dirpath = os.path.dirname(os.path.abspath(__file__)) 778 pyfiles = [os.path.join(dirpath, f) for f in os.listdir(dirpath) if 779 f.startswith("variables_") and f.endswith(".py")] 780 new = cls() 781 for pyf in pyfiles: 782 vd = InputVariables.from_pyfile(pyf) 783 new[vd.executable] = vd 784 785 return new 786 787 def iter_allvars(self): 788 """Iterate over all variables. Flat view.""" 789 for vd in self.values(): 790 for var in vd.values(): 791 yield var 792 793 def get_version_endpoints(self): 794 """ 795 API used by the webser to serve the documentation of a variable given codename, varname, [version]: 796 797 docs.abinit.org/vardocs/abinit/asr?version=8.6.2 798 799 # asr@anaddb at /variables/anaddb#asr 800 # asr@abinit at /variables/eph#asr 801 # asr@abinit at /variables/abinit/eph#asr 802 """ 803 code_urls = {} 804 for codename, vard in self.items(): 805 code_urls[codename] = d = {} 806 for vname, var in var.items(): 807 # This is the internal convention used to build the mkdocs site. 808 d[vname] = "/variables/%s/%s#%s" % (codename, var.varset, var.name) 809 # TODO: version and change mkdocs.yml 810 return version, code_urls 811 812 def update_json_endpoints(self, json_path, indent=4): 813 """ 814 Update the json file with the mapping varname --> relative url 815 used by the webserve to implement the `vardocs` API. 816 """ 817 with open(json_path, "rt") as fh: 818 oldd = json.load(fh) 819 820 new_version, newd = self.get_version_endpoints() 821 assert new_version not in oldd 822 oldd[new_version] = newd 823 with open(json_path, "wt") as fh: 824 json.dump(oldd, fh, indent=indent) 825 826 def _write_pymods(self, dirpath="."): 827 """ 828 Internal method used to regenerate the python modules. 829 """ 830 dirpath = os.path.abspath(dirpath) 831 from pprint import pformat 832 def nones2arg(obj, must_be_string=False): 833 if obj is None: 834 if must_be_string: 835 raise TypeError("obj must be string.") 836 return None 837 elif isinstance(obj, str): 838 s = str(obj).rstrip() 839 if "\n" in s: return '"""%s"""' % s 840 if "'" in s: return '"%s"' % s 841 if '"' in s: return "'%s'" % s 842 return '"%s"' % s 843 else: 844 raise TypeError("%s: %s" % (type(obj), str(obj))) 845 846 def topics2arg(obj): 847 if isinstance(obj, str): 848 if "," in obj: 849 obj = [s.strip() for s in obj.split(",")] 850 else: 851 obj = [obj] 852 if isinstance(obj, (list, tuple)): return pformat(obj) 853 854 raise TypeError("%s: %s" % (type(obj), str(obj))) 855 856 def dimensions2arg(obj): 857 if isinstance(obj, str) and obj == "scalar": return '"scalar"' 858 if isinstance(obj, (ValueWithUnit, MultipleValue, Range, ValueWithConditions)): 859 return "%s(%s)" % (obj.__class__.__name__, pformat(obj.__dict__)) 860 if isinstance(obj, (list, tuple)): return pformat(obj) 861 862 raise TypeError("%s, %s" % (type(obj), str(obj))) 863 864 def defaultval2arg(obj): 865 if obj is None: return obj 866 if isinstance(obj, (ValueWithUnit, MultipleValue, Range, ValueWithConditions)): 867 return "%s(%s)" % (obj.__class__.__name__, pformat(obj.__dict__)) 868 if isinstance(obj, (list, tuple)): return pformat(obj) 869 if isinstance(obj, str): return '"%s"' % str(obj) 870 if isinstance(obj, (int, float)): return obj 871 872 raise TypeError("%s, %s" % (type(obj), str(obj))) 873 874 for code in self: 875 varsd = self[code] 876 877 lines = ["""\ 878from __future__ import print_function, division, unicode_literals, absolute_import 879 880from abimkdocs.variables import ValueWithUnit, MultipleValue, Range 881ValueWithConditions = dict 882 883Variable=dict\nvariables = [""" 884] 885 for name in sorted(varsd.keys()): 886 var = varsd[name] 887 text = '"""\n' + var.text.rstrip() + '\n"""' 888 s = """\ 889Variable( 890 abivarname={abivarname}, 891 varset={varset}, 892 vartype={vartype}, 893 topics={topics}, 894 dimensions={dimensions}, 895 defaultval={defaultval}, 896 mnemonics={mnemonics}, 897 characteristics={characteristics}, 898 excludes={excludes}, 899 requires={requires}, 900 commentdefault={commentdefault}, 901 commentdims={commentdims}, 902 added_in_version=None, 903 alternative_name=None, 904 text={text}, 905), 906""".format(vartype='"%s"' % var.vartype, 907 characteristics=None if var.characteristics is None else pformat(var.characteristics), 908 mnemonics=nones2arg(var.mnemonics, must_be_string=True), 909 requires=nones2arg(var.requires), 910 excludes=nones2arg(var.excludes), 911 dimensions=dimensions2arg(var.dimensions), 912 varset='"%s"' % var.varset, 913 abivarname='"%s"' % var.abivarname, 914 commentdefault=nones2arg(var.commentdefault), 915 topics=topics2arg(var.topics), 916 commentdims=nones2arg(var.commentdims), 917 defaultval=defaultval2arg(var.defaultval), 918 added_in_version=var.added_in_version, 919 alternative_name=var.alternative_name, 920 text=text, 921 ) 922 923 lines.append(s) 924 #print(s) 925 926 lines.append("]") 927 # Write file 928 with open(os.path.join(dirpath, "variables_%s.py" % code), "wt") as fh: 929 fh.write("\n".join(lines)) 930 fh.write("\n") 931 932 933class InputVariables(OrderedDict): 934 """ 935 Dictionary storing the variables used by one executable. 936 937 .. attributes: 938 939 executable: Name of executable e.g. anaddb 940 """ 941 @classmethod 942 def from_pyfile(cls, filepath): 943 """Initialize the object from python file.""" 944 #import imp 945 #module = imp.load_source(filepath, filepath) 946 from importlib.machinery import SourceFileLoader 947 module = SourceFileLoader(filepath, filepath).load_module() 948 949 vlist = [Variable(**d) for d in module.variables] 950 new = cls() 951 new.executable = module.executable 952 for v in sorted(vlist, key=lambda v: v.name): 953 new[v.name] = v 954 return new 955 956 @lazy_property 957 def my_varset_list(self): 958 """Set with the all the varset strings found in the database.""" 959 return sorted(set(v.varset for v in self.values())) 960 961 @lazy_property 962 def name2varset(self): 963 """Dictionary mapping the name of the variable to the varset section.""" 964 d = {} 965 for name, var in self.items(): 966 d[name] = var.varset 967 return d 968 969 @lazy_property 970 def my_characteristics(self): 971 """Set with all characteristics found in the database. NB [] are removed from the string.""" 972 allchars = [] 973 for var in self.values(): 974 if var.characteristics is not None: 975 allchars.extend([c.replace("[", "").replace("]", "") for c in var.characteristics]) 976 return set(allchars) 977 978 def get_all_vnames(self, with_internal=False): 979 """ 980 Return set with all the variable names including possible aliases. 981 """ 982 doc_vnames = [] 983 for name, var in self.items(): 984 if not with_internal and var.is_internal: continue 985 doc_vnames.append(name) 986 if var.alternative_name is not None: 987 doc_vnames.append(var.alternative_name) 988 return set(doc_vnames) 989 990 def groupby_first_letter(self): 991 """Return ordered dict mapping first_char --> list of variables.""" 992 keys = sorted(self.keys(), key=lambda n: n[0].upper()) 993 od = OrderedDict() 994 for char, group in groupby(keys, key=lambda n: n[0].upper()): 995 od[char] = [self[name] for name in group] 996 return od 997 998 def get_vartabs_html(self, website, page_rpath): 999 """Return HTML string with all the variabes in tabular format.""" 1000 ch2vars = self.groupby_first_letter() 1001 ch2vars["All"] = self.values() 1002 # http://getbootstrap.com/javascript/#tabs 1003 html = """\ 1004<div> 1005<!-- Nav tabs --> 1006<ul class="nav nav-pills" role="tablist">\n""" 1007 1008 idname = self.executable + "-tabs" 1009 for i, char in enumerate(ch2vars): 1010 id_char = "#%s-%s" % (idname, char) 1011 if i == 0: 1012 html += """\n 1013<li role="presentation" class="active"><a href="%s" role="tab" data-toggle="tab">%s</a></li>\n""" % (id_char, char) 1014 else: 1015 html += """\ 1016<li role="presentation"><a href="%s" role="tab" data-toggle="tab">%s</a></li>\n""" % (id_char, char) 1017 html += """\ 1018</ul> 1019<!-- Tab panes --> 1020<div class="tab-content"> 1021 """ 1022 for i, (char, vlist) in enumerate(ch2vars.items()): 1023 id_char = "%s-%s" % (idname, char) 1024 p = " ".join(v.internal_link(website, page_rpath, cls="small-grey-link") for v in vlist) 1025 if i == 0: 1026 html += '<div role="tabpanel" class="tab-pane active" id="%s">\n%s\n</div>\n' % (id_char, p) 1027 else: 1028 html += '<div role="tabpanel" class="tab-pane" id="%s">\n%s\n</div>\n' % (id_char, p) 1029 1030 return html + "</div></div>" 1031 1032 def group_by_varset(self, names): 1033 """ 1034 Group a list of variable in sections. 1035 1036 Args: 1037 names: string or list of strings with ABINIT variable names. 1038 1039 Return: 1040 Ordered dict mapping section_name to the list of variable names belonging to the section. 1041 The dict uses the same ordering as those in `self.sections` 1042 """ 1043 d = defaultdict(list) 1044 1045 for name in list_strings(names): 1046 try: 1047 sec = self.name2varset[name] 1048 d[sec].append(name) 1049 except KeyError as exc: 1050 msg = ("`%s` is not a registered variable of code `%s`.\nPerhaps you are using an old " + 1051 "version of the database with a more recent Abinit?") % (name, self.executable) 1052 raise KeyError(msg) 1053 1054 return OrderedDict([(sec, d[sec]) for sec in self.my_varset_list if d[sec]]) 1055 1056 def apropos(self, varname): 1057 """Return the list of :class:`Variable` objects that are related` to the given varname""" 1058 var_list = [] 1059 for v in self.values(): 1060 if (v.text and varname in v.text or 1061 (v.dimensions is not None and varname in str(v.dimensions)) or 1062 (v.requires is not None and varname in v.requires) or 1063 (v.excludes is not None and varname in v.excludes)): 1064 var_list.append(v) 1065 1066 return var_list 1067 1068 def vars_with_varset(self, sections): 1069 """ 1070 List of :class:`Variable` associated to the given sections. 1071 sections can be a string or a list of strings. 1072 """ 1073 sections = set(list_strings(sections)) 1074 varlist = [] 1075 for v in self.values(): 1076 if v.varset in sections: 1077 varlist.append(v) 1078 1079 if not varlist: 1080 # Preventive check because someone may change the Abinit varset 1081 # thus breaking client code. 1082 raise ValueError("Empty varlist. Maybe someone changed varset again or wrong names in sections. %s" % str(sections)) 1083 1084 return varlist 1085 1086 def vars_with_char(self, chars): 1087 """ 1088 Return list of :class:`Variable` with the specified characteristic. 1089 chars can be a string or a list of strings. 1090 """ 1091 chars = ["[[" + c + "]]" for c in list_strings(chars)] 1092 varlist = [] 1093 for v in self.values(): 1094 if v.characteristics is None: continue 1095 if any(c in v.characteristics for c in chars): 1096 varlist.append(v) 1097 1098 return varlist 1099 1100 def get_graphviz_varname(self, varname, engine="automatic", graph_attr=None, node_attr=None, edge_attr=None): 1101 """ 1102 Generate task graph in the DOT language (only parents and children of this task). 1103 1104 Args: 1105 varname: Name of the variable. 1106 engine: ['dot', 'neato', 'twopi', 'circo', 'fdp', 'sfdp', 'patchwork', 'osage'] 1107 graph_attr: Mapping of (attribute, value) pairs for the graph. 1108 node_attr: Mapping of (attribute, value) pairs set for all nodes. 1109 edge_attr: Mapping of (attribute, value) pairs set for all edges. 1110 1111 Returns: graphviz.Digraph <https://graphviz.readthedocs.io/en/stable/api.html#digraph> 1112 """ 1113 var = self[varname] 1114 1115 # https://www.graphviz.org/doc/info/ 1116 from graphviz import Digraph 1117 graph = Digraph("task", engine="dot" if engine == "automatic" else engine) 1118 #graph.attr(label=repr(var)) 1119 #graph.node_attr.update(color='lightblue2', style='filled') 1120 #cluster_kwargs = dict(rankdir="LR", pagedir="BL", style="rounded", bgcolor="azure2") 1121 1122 # These are the default attrs for graphviz 1123 default_graph_attr = { 1124 'rankdir': 'LR', 1125 #'size': "8.0, 12.0", 1126 } 1127 if graph_attr is None: graph_attr = default_graph_attr 1128 1129 default_node_attr = { 1130 #'shape': 'box', 1131 #'fontsize': 10, 1132 #'height': 0.25, 1133 #'fontname': '"Vera Sans, DejaVu Sans, Liberation Sans, ' 1134 # 'Arial, Helvetica, sans"', 1135 #'style': '"setlinewidth(0.5)"', 1136 } 1137 if node_attr is None: node_attr = default_node_attr 1138 1139 default_edge_attr = { 1140 #'arrowsize': '0.5', 1141 #'style': '"setlinewidth(0.5)"', 1142 } 1143 if edge_attr is None: edge_attr = default_edge_attr 1144 1145 # Add input attributes. 1146 graph.graph_attr.update(**graph_attr) 1147 graph.node_attr.update(**node_attr) 1148 graph.edge_attr.update(**edge_attr) 1149 1150 def node_kwargs(var): 1151 return dict( 1152 shape="box", 1153 fontsize="10", 1154 height="0.25", 1155 #color=var.color_hex, 1156 label=str(var), 1157 URL=var.website_url, 1158 target="_top", 1159 tooltip=str(var.mnemonics), 1160 ) 1161 1162 edge_kwargs = dict(arrowType="vee", style="solid") 1163 1164 graph.node(var.name, **node_kwargs(var)) 1165 for parent in var.get_parent_names(): 1166 parent = self[parent] 1167 graph.node(parent.name, **node_kwargs(parent)) 1168 graph.edge(parent.name, var.name, **edge_kwargs) #, label=edge_label, color=self.color_hex 1169 1170 with_children = True 1171 if with_children: # > threshold 1172 # Connect task to children. 1173 for oname, ovar in self.items(): 1174 if oname == varname: continue 1175 if varname not in ovar.get_parent_names(): continue 1176 graph.node(ovar.name, **node_kwargs(ovar)) 1177 graph.edge(var.name, ovar.name, **edge_kwargs) #, label=edge_label, color=self.color_hex 1178 1179 return graph 1180 1181 def get_graphviz(self, varset=None, vartype=None, engine="automatic", graph_attr=None, node_attr=None, edge_attr=None): 1182 """ 1183 Generate graph in the DOT language (only parents and children of this task). 1184 1185 Args: 1186 varset: Select variables with this `varset`. Include all if None 1187 vartype: Select variables with this `vartype`. Include all 1188 engine: ['dot', 'neato', 'twopi', 'circo', 'fdp', 'sfdp', 'patchwork', 'osage'] 1189 graph_attr: Mapping of (attribute, value) pairs for the graph. 1190 node_attr: Mapping of (attribute, value) pairs set for all nodes. 1191 edge_attr: Mapping of (attribute, value) pairs set for all edges. 1192 1193 Returns: graphviz.Digraph <https://graphviz.readthedocs.io/en/stable/api.html#digraph> 1194 """ 1195 # https://www.graphviz.org/doc/info/ 1196 from graphviz import Digraph 1197 graph = Digraph("task", engine="dot" if engine == "automatic" else engine) 1198 #graph.attr(label=repr(var)) 1199 #graph.node_attr.update(color='lightblue2', style='filled') 1200 #cluster_kwargs = dict(rankdir="LR", pagedir="BL", style="rounded", bgcolor="azure2") 1201 1202 # These are the default attrs for graphviz 1203 default_graph_attr = { 1204 'rankdir': 'LR', 1205 #'size': "8.0, 12.0", 1206 } 1207 if graph_attr is None: graph_attr = default_graph_attr 1208 1209 default_node_attr = { 1210 #'shape': 'box', 1211 #'fontsize': 10, 1212 #'height': 0.25, 1213 #'fontname': '"Vera Sans, DejaVu Sans, Liberation Sans, ' 1214 # 'Arial, Helvetica, sans"', 1215 #'style': '"setlinewidth(0.5)"', 1216 } 1217 if node_attr is None: node_attr = default_node_attr 1218 1219 default_edge_attr = { 1220 #'arrowsize': '0.5', 1221 #'style': '"setlinewidth(0.5)"', 1222 } 1223 if edge_attr is None: edge_attr = default_edge_attr 1224 1225 # Add input attributes. 1226 graph.graph_attr.update(**graph_attr) 1227 graph.node_attr.update(**node_attr) 1228 graph.edge_attr.update(**edge_attr) 1229 1230 def node_kwargs(var): 1231 return dict( 1232 shape="box", 1233 fontsize="10", 1234 height="0.25", 1235 #color=var.color_hex, 1236 label=str(var), 1237 URL=var.website_url, 1238 target="_top", 1239 tooltip=str(var.mnemonics), 1240 ) 1241 1242 edge_kwargs = dict(arrowType="vee", style="solid") 1243 with_children = False 1244 1245 for name, var in self.items(): 1246 if vartype is not None and var.vartype != vartype: continue 1247 if varset is not None and var.varset != varset: continue 1248 1249 graph.node(var.name, **node_kwargs(var)) 1250 for parent in var.get_parent_names(): 1251 parent = self[parent] 1252 graph.node(parent.name, **node_kwargs(parent)) 1253 graph.edge(parent.name, var.name, **edge_kwargs) #, label=edge_label, color=self.color_hex 1254 1255 if with_children: # > threshold 1256 # Connect task to children. 1257 for oname, ovar in self.items(): 1258 if oname == varname: continue 1259 if varname not in ovar.get_parent_names(): continue 1260 graph.node(ovar.name, **node_kwargs(ovar)) 1261 graph.edge(var.name, ovar.name, **edge_kwargs) #, label=edge_label, color=self.color_hex 1262 1263 return graph 1264