Exemple #1
0
    def add_parameter(
            self,
            param_type,
            phase_name,  #pylint: disable=R0913
            constituent_array,
            param_order,
            param,
            ref=None,
            diffusing_species=None,
            force_insert=True):
        """
        Add a parameter.

        Parameters
        ----------
        param_type : str
            Type name of the parameter, e.g., G, L, BMAGN.
        phase_name : str
            Name of the phase.
        constituent_array : list
            Configuration of the sublattices (elements and/or species).
        param_order : int
            Polynomial order of the parameter.
        param : object
            Abstract representation of the parameter, e.g., in SymPy format.
        ref : str, optional
            Reference for the parameter.
        diffusing_species : str, optional
            (If kinetic parameter) Diffusing species for this parameter.
        force_insert : bool, optional
            If True, inserts into the database immediately. False is a delayed insert (for performance).

        Examples
        --------
        None yet.
        """
        species_dict = {s.name: s for s in self.species}
        new_parameter = {
            'phase_name':
            phase_name,
            'constituent_array':
            tuple(
                tuple(species_dict.get(s.upper(), Species(s)) for s in xs)
                for xs in constituent_array),  # must be hashable type
            'parameter_type':
            param_type,
            'parameter_order':
            param_order,
            'parameter':
            param,
            'diffusing_species':
            Species(diffusing_species),
            'reference':
            ref
        }
        if force_insert:
            self._parameters.insert(new_parameter)
        else:
            self._parameter_queue.append(new_parameter)
Exemple #2
0
def test_species_are_parsed_in_tdb_phases_and_parameters():
    """Species defined in the tdb phases and parameters should be parsed."""
    tdb_str = """
ELEMENT /-   ELECTRON_GAS              0.0000E+00  0.0000E+00  0.0000E+00!
ELEMENT VA   VACUUM                    0.0000E+00  0.0000E+00  0.0000E+00!
ELEMENT AL   FCC_A1                    2.6982E+01  4.5773E+03  2.8321E+01!
ELEMENT O    1/2_MOLE_O2(G)            1.5999E+01  4.3410E+03  1.0252E+02!

SPECIES AL+3                        AL1/+3!
SPECIES O-2                         O1/-2!
SPECIES O2                          O2!
SPECIES AL2                         AL2!


 PHASE TEST_PH % 1 1 !
 CONSTITUENT TEST_PH :AL,AL2,O-2: !
 PARA G(TEST_PH,AL;0) 298.15          +10; 6000 N !
 PARA G(TEST_PH,AL2;0) 298.15          +100; 6000 N !
 PARA G(TEST_PH,O-2;0) 298.15          +1000; 6000 N !

 PHASE T2SL % 2 1 1 !
  CONSTITUENT T2SL :AL+3:O-2: !
  PARA L(T2SL,AL+3:O-2;0) 298.15 +2; 6000 N !
    """
    from tinydb import where
    test_dbf = Database.from_string(tdb_str, fmt='tdb')
    written_tdb_str = test_dbf.to_string(fmt='tdb')
    test_dbf_reread = Database.from_string(written_tdb_str, fmt='tdb')
    assert set(test_dbf_reread.phases.keys()) == {'TEST_PH', 'T2SL'}
    assert test_dbf_reread.phases['TEST_PH'].constituents[0] == {
        Species('AL'), Species('AL2'),
        Species('O-2')
    }
    assert len(
        test_dbf_reread._parameters.search(
            where('constituent_array') == ((Species('AL'), ), ))) == 1
    assert len(
        test_dbf_reread._parameters.search(
            where('constituent_array') == ((Species('AL2'), ), ))) == 1
    assert len(
        test_dbf_reread._parameters.search(
            where('constituent_array') == ((Species('O-2'), ), ))) == 1

    assert test_dbf_reread.phases['T2SL'].constituents == ({Species('AL+3')},
                                                           {Species('O-2')})
    assert len(
        test_dbf_reread._parameters.search(
            where('constituent_array') == ((Species('AL+3'), ),
                                           (Species('O-2'), )))) == 1
Exemple #3
0
def _process_species(db, sp_name, sp_comp, charge=0, *args):
    """Add a species to the Database. If charge not specified, the Species will be neutral."""
    # process the species composition list of [element1, ratio1, element2, ratio2, ..., elementN, ratioN]
    constituents = {
        sp_comp[i]: sp_comp[i + 1]
        for i in range(0, len(sp_comp), 2)
    }
    db.species.add(Species(sp_name, constituents, charge=charge))
Exemple #4
0
    def __new__(cls, *args):  #pylint: disable=W0221
        new_self = None
        varname = None
        phase_name = None
        species = None
        if len(args) == 1:
            # this is an overall composition variable
            species = Species(args[0])
            varname = 'W_' + species.escaped_name.upper()
        elif len(args) == 2:
            # this is a phase-specific composition variable
            phase_name = args[0].upper()
            species = Species(args[1])
            varname = 'W_' + phase_name + '_' + species.escaped_name.upper()
        else:
            # not defined
            raise ValueError('Weight fraction not defined for args: ' + args)

        #pylint: disable=E1121
        new_self = StateVariable.__new__(cls, varname, nonnegative=True)
        new_self.phase_name = phase_name
        new_self.species = species
        return new_self
Exemple #5
0
 def write_parameter(param_to_write):
     constituents = ':'.join([
         ','.join(sorted([i.name.upper() for i in subl]))
         for subl in param_to_write.constituent_array
     ])
     # TODO: Handle references
     paramx = param_to_write.parameter
     if not isinstance(paramx, Piecewise):
         # Non-piecewise parameters need to be wrapped to print correctly
         # Otherwise TC's TDB parser will fail
         paramx = Piecewise((paramx, And(v.T >= 1, v.T < 10000)))
     exprx = TCPrinter().doprint(paramx).upper()
     if ';' not in exprx:
         exprx += '; N'
     if param_to_write.diffusing_species != Species(None):
         ds = "&" + param_to_write.diffusing_species.name
     else:
         ds = ""
     return "PARAMETER {}({}{},{};{}) {} !\n".format(
         param_to_write.parameter_type.upper(),
         param_to_write.phase_name.upper(), ds, constituents,
         param_to_write.parameter_order, exprx)
Exemple #6
0
def write_tdb(dbf, fd, groupby='subsystem', if_incompatible='warn'):
    """
    Write a TDB file from a pycalphad Database object.

    The goal is to produce TDBs that conform to the most restrictive subset of database specifications. Some of these
    can be adjusted for automatically, such as the Thermo-Calc line length limit of 78. Others require changing the
    database in non-trivial ways, such as the maximum length of function names (8). The default is to warn the user when
    attempting to write an incompatible database and the user must choose whether to warn and write the file anyway or
    to fix the incompatibility.

    Currently the supported compatibility fixes are:
    - Line length <= 78 characters (Thermo-Calc)
    - Function names <= 8 characters (Thermo-Calc)

    The current unsupported fixes include:
    - Keyword length <= 2000 characters (Thermo-Calc)
    - Element names <= 2 characters (Thermo-Calc)
    - Phase names <= 24 characters (Thermo-Calc)

    Other TDB compatibility issues required by Thermo-Calc or other software should be reported to the issue tracker.

    Parameters
    ----------
    dbf : Database
        A pycalphad Database.
    fd : file-like
        File descriptor.
    groupby : ['subsystem', 'phase'], optional
        Desired grouping of parameters in the file.
    if_incompatible : string, optional ['raise', 'warn', 'fix']
        Strategy if the database does not conform to the most restrictive database specification.
        The 'warn' option (default) will write out the incompatible database with a warning.
        The 'raise' option will raise a DatabaseExportError.
        The 'ignore' option will write out the incompatible database silently.
        The 'fix' option will rectify the incompatibilities e.g. through name mangling.
    """
    # Before writing anything, check that the TDB is valid and take the appropriate action if not
    if if_incompatible not in ['warn', 'raise', 'ignore', 'fix']:
        raise ValueError(
            'Incorrect options passed to \'if_invalid\'. Valid args are \'raise\', \'warn\', or \'fix\'.'
        )
    # Handle function names > 8 characters
    long_function_names = {k for k in dbf.symbols.keys() if len(k) > 8}
    if len(long_function_names) > 0:
        if if_incompatible == 'raise':
            raise DatabaseExportError(
                'The following function names are beyond the 8 character TDB limit: {}. Use the keyword argument \'if_incompatible\' to control this behavior.'
                .format(long_function_names))
        elif if_incompatible == 'fix':
            # if we are going to make changes, make the changes to a copy and leave the original object untouched
            dbf = deepcopy(
                dbf)  # TODO: if we do multiple fixes, we should only copy once
            symbol_name_map = {}
            for name in long_function_names:
                hashed_name = 'F' + str(
                    hashlib.md5(name.encode('UTF-8')).hexdigest()).upper(
                    )[:7]  # this is implictly upper(), but it is explicit here
                symbol_name_map[name] = hashed_name
            _apply_new_symbol_names(dbf, symbol_name_map)
        elif if_incompatible == 'warn':
            warnings.warn(
                'Ignoring that the following function names are beyond the 8 character TDB limit: {}. Use the keyword argument \'if_incompatible\' to control this behavior.'
                .format(long_function_names))
    # Begin constructing the written database
    writetime = datetime.datetime.now()
    maxlen = 78
    output = ""
    # Comment header block
    # Import here to prevent circular imports
    from pycalphad import __version__
    output += ("$" * maxlen) + "\n"
    output += "$ Date: {}\n".format(writetime.strftime("%Y-%m-%d %H:%M"))
    output += "$ Components: {}\n".format(', '.join(sorted(dbf.elements)))
    output += "$ Phases: {}\n".format(', '.join(sorted(dbf.phases.keys())))
    output += "$ Generated by {} (pycalphad {})\n".format(
        getpass.getuser(), __version__)
    output += ("$" * maxlen) + "\n\n"
    for element in sorted(dbf.elements):
        ref = dbf.refstates.get(element, {})
        refphase = ref.get('phase', 'BLANK')
        mass = ref.get('mass', 0.0)
        H298 = ref.get('H298', 0.0)
        S298 = ref.get('S298', 0.0)
        output += "ELEMENT {0} {1} {2} {3} {4} !\n".format(
            element.upper(), refphase, mass, H298, S298)
    if len(dbf.elements) > 0:
        output += "\n"
    for species in sorted(dbf.species, key=lambda s: s.name):
        if species.name not in dbf.elements:
            # construct the charge part of the specie
            if species.charge != 0:
                if species.charge > 0:
                    charge_sign = '+'
                else:
                    charge_sign = ''
                charge = '/{}{}'.format(charge_sign, species.charge)
            else:
                charge = ''
            species_constituents = ''.join([
                '{}{}'.format(el, val)
                for el, val in sorted(species.constituents.items(),
                                      key=lambda t: t[0])
            ])
            output += "SPECIES {0} {1}{2} !\n".format(species.name.upper(),
                                                      species_constituents,
                                                      charge)
    if len(dbf.species) > 0:
        output += "\n"
    # Write FUNCTION block
    for name, expr in sorted(dbf.symbols.items()):
        if not isinstance(expr, Piecewise):
            # Non-piecewise exprs need to be wrapped to print
            # Otherwise TC's TDB parser will complain
            expr = Piecewise((expr, And(v.T >= 1, v.T < 10000)))
        expr = TCPrinter().doprint(expr).upper()
        if ';' not in expr:
            expr += '; N'
        output += "FUNCTION {0} {1} !\n".format(name.upper(), expr)
    output += "\n"
    # Boilerplate code
    output += "TYPE_DEFINITION % SEQ * !\n"
    output += "DEFINE_SYSTEM_DEFAULT ELEMENT 2 !\n"
    default_elements = [
        i.upper() for i in sorted(dbf.elements)
        if i.upper() == 'VA' or i.upper() == '/-'
    ]
    if len(default_elements) > 0:
        output += 'DEFAULT_COMMAND DEFINE_SYSTEM_ELEMENT {} !\n'.format(
            ' '.join(default_elements))
    output += "\n"
    typedef_chars = list("^&*()'ABCDEFGHIJKLMNOPQSRTUVWXYZ")[::-1]
    #  Write necessary TYPE_DEF based on model hints
    typedefs = defaultdict(lambda: ["%"])
    for name, phase_obj in sorted(dbf.phases.items()):
        model_hints = phase_obj.model_hints.copy()
        possible_options = set(phase_options.keys()).intersection(model_hints)
        # Phase options are handled later
        for option in possible_options:
            del model_hints[option]
        if ('ordered_phase'
                in model_hints.keys()) and (model_hints['ordered_phase']
                                            == name):
            new_char = typedef_chars.pop()
            typedefs[name].append(new_char)
            typedefs[model_hints['disordered_phase']].append(new_char)
            output += 'TYPE_DEFINITION {} GES AMEND_PHASE_DESCRIPTION {} DISORDERED_PART {} !\n'\
                .format(new_char, model_hints['ordered_phase'].upper(),
                        model_hints['disordered_phase'].upper())
            del model_hints['ordered_phase']
            del model_hints['disordered_phase']
        if ('disordered_phase'
                in model_hints.keys()) and (model_hints['disordered_phase']
                                            == name):
            # We handle adding the correct typedef when we write the ordered phase
            del model_hints['ordered_phase']
            del model_hints['disordered_phase']
        if 'ihj_magnetic_afm_factor' in model_hints.keys():
            new_char = typedef_chars.pop()
            typedefs[name].append(new_char)
            output += 'TYPE_DEFINITION {} GES AMEND_PHASE_DESCRIPTION {} MAGNETIC {} {} !\n'\
                .format(new_char, name.upper(), model_hints['ihj_magnetic_afm_factor'],
                        model_hints['ihj_magnetic_structure_factor'])
            del model_hints['ihj_magnetic_afm_factor']
            del model_hints['ihj_magnetic_structure_factor']
        if len(model_hints) > 0:
            # Some model hints were not properly consumed
            raise ValueError(
                'Not all model hints are supported: {}'.format(model_hints))
    # Perform a second loop now that all typedefs / model hints are consistent
    for name, phase_obj in sorted(dbf.phases.items()):
        # model_hints may also contain "phase options", e.g., ionic liquid
        model_hints = phase_obj.model_hints.copy()
        name_with_options = str(name.upper())
        possible_options = set(phase_options.keys()).intersection(
            model_hints.keys())
        if len(possible_options) > 0:
            name_with_options += ':'
        for option in possible_options:
            name_with_options += phase_options[option]
        output += "PHASE {0} {1}  {2} {3} !\n".format(
            name_with_options, ''.join(typedefs[name]),
            len(phase_obj.sublattices),
            ' '.join([str(i) for i in phase_obj.sublattices]))
        constituents = ':'.join([
            ','.join([spec.name for spec in sorted(subl)])
            for subl in phase_obj.constituents
        ])
        output += "CONSTITUENT {0} :{1}: !\n".format(name_with_options,
                                                     constituents)
        output += "\n"

    # PARAMETERs by subsystem
    param_sorted = defaultdict(lambda: list())
    paramtuple = namedtuple('ParamTuple', [
        'phase_name', 'parameter_type', 'complexity', 'constituent_array',
        'parameter_order', 'diffusing_species', 'parameter', 'reference'
    ])
    for param in dbf._parameters.all():
        if groupby == 'subsystem':
            components = set()
            for subl in param['constituent_array']:
                components |= set(subl)
            if param['diffusing_species'] != Species(None):
                components |= {param['diffusing_species']}
            # Wildcard operator is not a component
            components -= {'*'}
            desired_active_pure_elements = [
                list(x.constituents.keys()) for x in components
            ]
            components = set([
                el.upper() for constituents in desired_active_pure_elements
                for el in constituents
            ])
            # Remove vacancy if it's not the only component (pure vacancy endmember)
            if len(components) > 1:
                components -= {'VA'}
            components = tuple(sorted([c.upper() for c in components]))
            grouping = components
        elif groupby == 'phase':
            grouping = param['phase_name'].upper()
        else:
            raise ValueError(
                'Unknown groupby attribute \'{}\''.format(groupby))
        # We use the complexity parameter to help with sorting the parameters logically
        param_sorted[grouping].append(
            paramtuple(param['phase_name'], param['parameter_type'],
                       sum([len(i) for i in param['constituent_array']]),
                       param['constituent_array'], param['parameter_order'],
                       param['diffusing_species'], param['parameter'],
                       param['reference']))

    def write_parameter(param_to_write):
        constituents = ':'.join([
            ','.join(sorted([i.name.upper() for i in subl]))
            for subl in param_to_write.constituent_array
        ])
        # TODO: Handle references
        paramx = param_to_write.parameter
        if not isinstance(paramx, Piecewise):
            # Non-piecewise parameters need to be wrapped to print correctly
            # Otherwise TC's TDB parser will fail
            paramx = Piecewise((paramx, And(v.T >= 1, v.T < 10000)))
        exprx = TCPrinter().doprint(paramx).upper()
        if ';' not in exprx:
            exprx += '; N'
        if param_to_write.diffusing_species != Species(None):
            ds = "&" + param_to_write.diffusing_species.name
        else:
            ds = ""
        return "PARAMETER {}({}{},{};{}) {} !\n".format(
            param_to_write.parameter_type.upper(),
            param_to_write.phase_name.upper(), ds, constituents,
            param_to_write.parameter_order, exprx)

    if groupby == 'subsystem':
        for num_species in range(1, 5):
            subsystems = list(
                itertools.combinations(
                    sorted([i.name.upper() for i in dbf.species]),
                    num_species))
            for subsystem in subsystems:
                parameters = sorted(param_sorted[subsystem])
                if len(parameters) > 0:
                    output += "\n\n"
                    output += "$" * maxlen + "\n"
                    output += "$ {}".format('-'.join(sorted(subsystem)).center(
                        maxlen, " ")[2:-1]) + "$\n"
                    output += "$" * maxlen + "\n"
                    output += "\n"
                    for parameter in parameters:
                        output += write_parameter(parameter)
        # Don't generate combinatorics for multi-component subsystems or we'll run out of memory
        if len(dbf.species) > 4:
            subsystems = [k for k in param_sorted.keys() if len(k) > 4]
            for subsystem in subsystems:
                parameters = sorted(param_sorted[subsystem])
                for parameter in parameters:
                    output += write_parameter(parameter)
    elif groupby == 'phase':
        for phase_name in sorted(dbf.phases.keys()):
            parameters = sorted(param_sorted[phase_name])
            if len(parameters) > 0:
                output += "\n\n"
                output += "$" * maxlen + "\n"
                output += "$ {}".format(phase_name.upper().center(
                    maxlen, " ")[2:-1]) + "$\n"
                output += "$" * maxlen + "\n"
                output += "\n"
                for parameter in parameters:
                    output += write_parameter(parameter)
    else:
        raise ValueError('Unknown groupby attribute {}'.format(groupby))
    # Reflow text to respect character limit per line
    fd.write(reflow_text(output, linewidth=maxlen))