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