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)
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
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))
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
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)
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))