Convert ChemKED record to ReSpecTh XML file.

        This converter uses common information in a ChemKED file to generate a
        ReSpecTh XML file. Note that some information may be lost, as ChemKED stores
        some additional attributes.

        Arguments:
            filename (`str`): Filename for output ReSpecTh XML file.

        Example:
            >>> dataset = ChemKED(yaml_file)
            >>> dataset.convert_to_ReSpecTh(xml_file)

def convert_to_ReSpecTh(self, filename):
        """Convert ChemKED record to ReSpecTh XML file.

        This converter uses common information in a ChemKED file to generate a
        ReSpecTh XML file. Note that some information may be lost, as ChemKED stores
        some additional attributes.

        Arguments:
            filename (`str`): Filename for output ReSpecTh XML file.

        Example:
            >>> dataset = ChemKED(yaml_file)
            >>> dataset.convert_to_ReSpecTh(xml_file)
        """
        root = etree.Element('experiment')

        file_author = etree.SubElement(root, 'fileAuthor')
        file_author.text = self.file_authors[0]['name']

        # right now ChemKED just uses an integer file version
        file_version = etree.SubElement(root, 'fileVersion')
        major_version = etree.SubElement(file_version, 'major')
        major_version.text = str(self.file_version)
        minor_version = etree.SubElement(file_version, 'minor')
        minor_version.text = '0'

        respecth_version = etree.SubElement(root, 'ReSpecThVersion')
        major_version = etree.SubElement(respecth_version, 'major')
        major_version.text = '1'
        minor_version = etree.SubElement(respecth_version, 'minor')
        minor_version.text = '0'

        # Only ignition delay currently supported
        exp = etree.SubElement(root, 'experimentType')
        if self.experiment_type == 'ignition delay':
            exp.text = 'Ignition delay measurement'
        else:
            raise NotImplementedError('Only ignition delay type supported for conversion.')

        reference = etree.SubElement(root, 'bibliographyLink')
        citation = ''
        for author in self.reference.authors:
            citation += author['name'] + ', '
        citation += (self.reference.journal + ' (' + str(self.reference.year) + ') ' +
                     str(self.reference.volume) + ':' + self.reference.pages + '. ' +
                     self.reference.detail
                     )
        reference.set('preferredKey', citation)
        reference.set('doi', self.reference.doi)

        apparatus = etree.SubElement(root, 'apparatus')
        kind = etree.SubElement(apparatus, 'kind')
        kind.text = self.apparatus.kind

        common_properties = etree.SubElement(root, 'commonProperties')
        # ChemKED objects have no common properties once loaded. Check for properties
        # among datapoints that tend to be common
        common = []
        composition = self.datapoints[0].composition

        # Composition type *has* to be the same
        composition_type = self.datapoints[0].composition_type
        if not all(dp.composition_type == composition_type for dp in self.datapoints):
            raise NotImplementedError('Error: ReSpecTh does not support varying composition '
                                      'type among datapoints.'
                                      )

        if all([composition == dp.composition for dp in self.datapoints]):
            # initial composition is common
            common.append('composition')
            prop = etree.SubElement(common_properties, 'property')
            prop.set('name', 'initial composition')

            for species_name, species in composition.items():
                component = etree.SubElement(prop, 'component')
                species_link = etree.SubElement(component, 'speciesLink')
                species_link.set('preferredKey', species_name)
                if species.InChI is not None:
                    species_link.set('InChI', species.InChI)

                amount = etree.SubElement(component, 'amount')
                amount.set('units', composition_type)
                amount.text = str(species.amount.magnitude)

        # If multiple datapoints present, then find any common properties. If only
        # one datapoint, then composition should be the only "common" property.
        if len(self.datapoints) > 1:
            for prop_name in datagroup_properties:
                attribute = prop_name.replace(' ', '_')
                quantities = [getattr(dp, attribute, False) for dp in self.datapoints]

                # All quantities must have the property in question and all the
                # values must be equal
                if all(quantities) and quantities.count(quantities[0]) == len(quantities):
                    common.append(prop_name)
                    prop = etree.SubElement(common_properties, 'property')
                    prop.set('description', '')
                    prop.set('name', prop_name)
                    prop.set('units', str(quantities[0].units))

                    value = etree.SubElement(prop, 'value')
                    value.text = str(quantities[0].magnitude)

        # Ignition delay can't be common, unless only a single datapoint.

        datagroup = etree.SubElement(root, 'dataGroup')
        datagroup.set('id', 'dg1')
        datagroup_link = etree.SubElement(datagroup, 'dataGroupLink')
        datagroup_link.set('dataGroupID', '')
        datagroup_link.set('dataPointID', '')

        property_idx = {}
        labels = {'temperature': 'T', 'pressure': 'P',
                  'ignition delay': 'tau', 'pressure rise': 'dP/dt',
                  }

        for prop_name in datagroup_properties:
            attribute = prop_name.replace(' ', '_')
            # This can't be hasattr because properties are set to the value None
            # if no value is specified in the file, so the attribute always exists
            prop_indices = [i for i, dp in enumerate(self.datapoints)
                            if getattr(dp, attribute) is not None
                            ]
            if prop_name in common or not prop_indices:
                continue

            prop = etree.SubElement(datagroup, 'property')
            prop.set('description', '')
            prop.set('name', prop_name)
            units = str(getattr(self.datapoints[prop_indices[0]], attribute).units)
            prop.set('units', units)
            idx = 'x{}'.format(len(property_idx) + 1)
            property_idx[idx] = {'name': prop_name, 'units': units}
            prop.set('id', idx)
            prop.set('label', labels[prop_name])

        # Need to handle datapoints with possibly different species in the initial composition
        if 'composition' not in common:
            for dp in self.datapoints:
                for species in dp.composition.values():
                    # Only add new property for species not already considered
                    has_spec = any([species.species_name in d.values()
                                    for d in property_idx.values()
                                    ])
                    if not has_spec:
                        prop = etree.SubElement(datagroup, 'property')
                        prop.set('description', '')

                        idx = 'x{}'.format(len(property_idx) + 1)
                        property_idx[idx] = {'name': species.species_name}
                        prop.set('id', idx)
                        prop.set('label', '[' + species.species_name + ']')
                        prop.set('name', 'composition')
                        prop.set('units', self.datapoints[0].composition_type)

                        species_link = etree.SubElement(prop, 'speciesLink')
                        species_link.set('preferredKey', species.species_name)
                        if species.InChI is not None:
                            species_link.set('InChI', species.InChI)

        for dp in self.datapoints:
            datapoint = etree.SubElement(datagroup, 'dataPoint')
            for idx, val in property_idx.items():
                # handle regular properties a bit differently than composition
                if val['name'] in datagroup_properties:
                    value = etree.SubElement(datapoint, idx)
                    quantity = getattr(dp, val['name'].replace(' ', '_')).to(val['units'])
                    value.text = str(quantity.magnitude)
                else:
                    # composition
                    for item in dp.composition.values():
                        if item.species_name == val['name']:
                            value = etree.SubElement(datapoint, idx)
                            value.text = str(item.amount.magnitude)

        # See https://stackoverflow.com/a/16097112 for the None.__ne__
        history_types = ['volume_history', 'temperature_history', 'pressure_history',
                         'piston_position_history', 'light_emission_history',
                         'OH_emission_history', 'absorption_history']
        time_histories = [getattr(dp, p) for dp in self.datapoints for p in history_types]
        time_histories = list(filter(None.__ne__, time_histories))

        if len(self.datapoints) > 1 and len(time_histories) > 1:
            raise NotImplementedError('Error: ReSpecTh files do not support multiple datapoints '
                                      'with a time history.')
        elif len(time_histories) > 0:
            for dg_idx, hist in enumerate(time_histories):
                if hist.type not in ['volume', 'temperature', 'pressure']:
                    warn('The time-history type {} is not supported by ReSpecTh for '
                         'ignition delay experiments'.format(hist.type))
                    continue

                datagroup = etree.SubElement(root, 'dataGroup')
                datagroup.set('id', 'dg{}'.format(dg_idx))
                datagroup_link = etree.SubElement(datagroup, 'dataGroupLink')
                datagroup_link.set('dataGroupID', '')
                datagroup_link.set('dataPointID', '')

                # Time history has two properties: time and quantity.
                prop = etree.SubElement(datagroup, 'property')
                prop.set('description', '')
                prop.set('name', 'time')
                prop.set('units', str(hist.time.units))
                time_idx = 'x{}'.format(len(property_idx) + 1)
                property_idx[time_idx] = {'name': 'time'}
                prop.set('id', time_idx)
                prop.set('label', 't')

                prop = etree.SubElement(datagroup, 'property')
                prop.set('description', '')
                prop.set('name', hist.type)
                prop.set('units', str(hist.quantity.units))
                quant_idx = 'x{}'.format(len(property_idx) + 1)
                property_idx[quant_idx] = {'name': hist.type}
                prop.set('id', quant_idx)
                prop.set('label', 'V')

                for time, quantity in zip(hist.time, hist.quantity):
                    datapoint = etree.SubElement(datagroup, 'dataPoint')
                    value = etree.SubElement(datapoint, time_idx)
                    value.text = str(time.magnitude)
                    value = etree.SubElement(datapoint, quant_idx)
                    value.text = str(quantity.magnitude)

        ign_types = [getattr(dp, 'ignition_type', False) for dp in self.datapoints]
        # All datapoints must have the same ignition target and type
        if all(ign_types) and ign_types.count(ign_types[0]) == len(ign_types):
            # In ReSpecTh files all datapoints must share ignition type
            ignition = etree.SubElement(root, 'ignitionType')
            if ign_types[0]['target'] in ['pressure', 'temperature']:
                ignition.set('target', ign_types[0]['target'][0].upper())
            else:
                # options left are species
                ignition.set('target', self.datapoints[0].ignition_type['target'])
            if ign_types[0]['type'] == 'd/dt max extrapolated':
                ignition.set('type', 'baseline max intercept from d/dt')
            else:
                ignition.set('type', self.datapoints[0].ignition_type['type'])
        else:
            raise NotImplementedError('Different ignition targets or types for multiple datapoints '
                                      'are not supported in ReSpecTh.')

        et = etree.ElementTree(root)
        et.write(filename, encoding='utf-8', xml_declaration=True)

        # now do a "pretty" rewrite
        xml = minidom.parse(filename)
        xml_string = xml.toprettyxml(indent='    ')
        with open(filename, 'w') as f:
            f.write(xml_string)

        print('Converted to ' + filename)