CentraleSupélecDépartement informatique
Plateau de Moulon
3 rue Joliot-Curie
F-91192 Gif-sur-Yvette cedex
1CC1000 - Information Systems and Programming - Lab: object-oriented programming

Presentation

We intend to model chemical reactions.

For example, in the presence of methane and oxygen molecules, a combustion reaction will produce carbon dioxide and water:

CH4 + O2 + O2 → CO2 + H2O + H2O

Here is a reminder of few basic notions:

  • An atom is made up of nucleons (protons and neutrons) and electrons. For this exercise, we'll ignore electrons.
    The mass number of an atom is its number of nucleons.
  • Atoms with the same number of protons are classified by a chemical element: hydrogen atoms have one proton,
    oxygen atoms have eight. The number of protons is called the atomic number.
  • Atoms characterized by the same chemical element may differ in the number of neutrons they contain. They are isotopes.
    For example, there are 3 isotopes of hydrogen: protium has no neutrons, deuterium has one neutron
    and tritium has 2 neutrons.
  • A molecule is a set of atoms (we won't take isomers into account).

Chemical Component

We'll call chemical component the entities that we intend to represent, i.e. atoms and molecules. A chemical component has a name and a mass (for an atom: the mass number, for a molecule: the sum of the mass numbers of its constituent atoms).

  • Define the ChemicalComponent class; as this class is abstract, it will be a subclass of the ABC class of the abc module.
  • Define the constructor of this class; it will receive the name as an argument. This name will not be modifiable after initialization, so it will be stored in a private attribute (starting with __), and accessible as a property (decorator @property on the accessor).
  • Define an abstract method (decorator @abstractmethod) mass().
  • Try to create an instance of ChemicalComponent, you should get an error.

ANSWER ELEMENTS

from abc import ABC, abstractmethod

class ChemicalComponent(ABC):
    def __init__(self, name):
        self.__name = name

    @property
    def name(self):
        return self.__name

    @abstractmethod
    def mass():
        pass

#chemical_component = ChemicalComponent("Test ChemicalComponent")

Chemical Element

A chemical element is a chemical component made of a single atom whose number of protons is known.

We could store this number in a non-modifiable attribute using the same approach as for the name. However, to make our code easier to use, we're going to directly propose the creation of a hydrogen atom, rather than the creation of an atom with a single proton. In other words, we'll have a Hydrogen class (the same goes for the other chemical elements). The number of protons will be a characteristic of this class, not of any particular instance of it. In other words, all hydrogen atoms have one proton.

Unlike a class method, a class attribute cannot be redefined in a subclass, so this is the solution we'll use.

A chemical element also defines a symbol (H for hydrogen, O for oxygen...), so we'll use the same approach for this attribute.

  • Define the ChemicalElement class; this class will also be abstract, a subclass of ChemicalComponent.
  • Define the constructor of this class, it will receive the name as an argument.
  • Define the class methods (decorator @classmethod) atomic_number() and symbol().
  • Check that it is still not possible to create an instance of this class.

ANSWER ELEMENTS

class ChemicalElement(ChemicalComponent):
    def __init__(self, name):
        super().__init__(name)

    @classmethod
    def symbol(cls):
        pass

    @classmethod
    def atomic_number(cls):
        pass

#chemical_element = ChemicalElement("Test ChemicalElement")

Atom

An atom is a chemical element whose number of neutrons is known. This number cannot be modified after initialization, and is stored in a private attribute.

  • Define the Atom class; this class is a subclass of the ChemicalElement class.
  • Define the constructor of this class; it takes the name and number of neutrons as arguments.
  • Define the neutron_number() method and redefine the mass() method with the correct semantics.
  • Check that it is now possible to create an instance of this class (example of instance: the deuterium), display its name and neutron number.
  • However, this class is not intended to be instantiated directly, as its number of protons is not known yet: check this by trying to display the mass of the atom you've created.

ANSWER ELEMENTS

class Atom(ChemicalElement):
    def __init__(self, name, neutron_number):
        super().__init__(name)
        self.__neutron_number = neutron_number

    def neutron_number(self):
        return self.__neutron_number

    def mass(self):
        return self.atomic_number() + self.neutron_number()

deuterium = Atom("Deuterium", 1)
print(deuterium.name)
print(deuterium.neutron_number())
#print(deuterium.mass())

Hydrogen

Hydrogen is the chemical element with atomic number 1 and symbol H.

There are three isotopes of hydrogen: protium has no neutrons (99.99% of the hydrogen present on Earth), deuterium has one (0.01%) and tritium has two (does not exist naturally, unstable).

  • Define the Hydrogen class; this class is a subclass of the Atom.
  • Define the constructor of this class, that takes as arguments the name (which by default will be Hydrogen) and the number of neutrons (0 by default). If the latter is invalid, a TypeError exception will be thrown.
  • Redefine the atomic_number() and symbol() methods with the correct semantics.
  • Create a deuterium atom and display its mass.
  • Display the created deuterium atom. The result looks as follows
  <__main__.Hydrogen object at 0x1043e6390>

It is not satisfactory.

We're not going to try to display it in the official form (2H), we'll just write it [1/2]H: the first number is the atomic number, the second is the mass number, and the letter is the symbol.

When Python wants to convert data into a character string to display it, it uses the __str__() method, which has a default definition in the object class, the root of the inheritance tree. We will now redefine this method to display the desired string.

  • In which class is it relevant to redefine this method? Add this redefinition.

ANSWER ELEMENTS

# Method added in class Atom
    def __str__(self):
        return f"[{self.atomic_number()}/{self.mass()}]{self.symbol()}"

class Hydrogen(Atom):
    def __init__(self, name = 'Hydrogen', neutron_number = 0):
        if not 0 <= neutron_number <= 2:
            raise TypeError("Invalid neutron number in Hydrogen")
        super().__init__(name, neutron_number)

    @classmethod
    def atomic_number(cls):
        return 1

    @classmethod
    def symbol(cls):
        return 'H'

deuterium = Hydrogen("Deuterium", 1)
print(deuterium.mass())

Other atoms

For the chemical reaction we wish to model, we need the atoms of carbon (symbol C, 6 protons, 6 to 8 neutrons, the most common isotope is 12C) and oxygen (symbol O, 8 protons, 6 to 20 neutrons, the most common isotope is 160).

  • Define the Carbon and Oxygen classes.

ANSWER ELEMENTS

class Carbon(Atom):
    def __init__(self, name = 'Carbon', neutron_number = 6):
        if not 6 <= neutron_number <= 8:
            raise TypeError("Invalid neutron number in Carbon")
        super().__init__(name, neutron_number)

    @classmethod
    def atomic_number(cls):
        return 6

    @classmethod
    def symbol(cls):
        return 'C'

class Oxygen(Atom):
    def __init__(self, name = 'Oxygen', neutron_number = 8):
        if not 6 <= neutron_number <= 20:
            raise TypeError("Invalid neutron number in Oxygen")
        super().__init__(name, neutron_number)

    @classmethod
    def atomic_number(cls):
        return 8

    @classmethod
    def symbol(cls):
        return 'O'

Molecule

A molecule is a chemical component made of at least two atoms.

  • Define the class Molecule, a subclass of ChemicalComponent.
  • Define the constructor of this class. It takes a name and a list of atoms as arguments, stored in a private attribute. The constructor checks that the list contains only atoms (otherwise, it raises a TypeError); to do this, you can use the function
isinstance(one_instance, OneClass)

which tests whether one_instance is an instance of the OneClass class or one of its subclasses.

  • Redefine the mass() method with correct semantics.
  • Create a water molecule, and display its mass.
  • Display this molecule, the result will not be not satisfactory.

The aim is to display it as :

  H2O

i.e. display each type of atom in the molecule, followed, if there are several of that type, by the number of occurrences. No attempt will be made to respect the order of atom types.

  • Define a method in the Molecule class that returns a dictionary describing the composition of a molecule in terms of atom types.
  • Redefine the __str__() method of this class to obtain the desired display.

Python classes can be used as dictionary keys.The class of an object is obtained by the function type().


ANSWER ELEMENTS

class Molecule(ChemicalComponent):
    def __init__(self, name, atoms):
        for atom in atoms:
            if not isinstance(atom, Atom):
                raise TypeError("Not an Atom")
        super().__init__(name)
        self.__atoms = atoms

    def mass(self):
        return sum([atom.mass() for atom in self.__atoms])

    def composition(self):
        d = {}
        for atom in self.__atoms:
            d[type(atom)] = d.get(type(atom), 0) + 1
        return d

    def __str__(self):
        return "".join([f"{a.symbol()}{'' if n == 1 else n}" for a, n in self.composition().items()])

water = Molecule("Water", [Hydrogen(), Hydrogen(), Oxygen()])
print(water.mass())
print(water)

Water and other molecules

We need the following kinds of molecules: methane, dioxygen, carbon dioxide, water.

  • Define the classes Methane, Dioxygen, CarbonDioxide and Water, subclasses of Molecule.
  • Define the constructors of these classes, they take as arguments a name, with the class name as default value, and the list of atoms; the default value of the list will be a list of the atoms composing these molecules. The constructor checks that the list contains only the correct atoms, and will throw a TypeError exception if this is not the case.

ANSWER ELEMENTS

class Methane(Molecule):
    def __init__(self, name = "Methane", atoms = [Carbon(), Hydrogen(), Hydrogen(), Hydrogen(), Hydrogen()]):
        super().__init__(name, atoms)
        d = self.composition()
        if len(d) != 2:
            raise TypeError("Bad atoms in methane")
        if not Hydrogen in d.keys() or d[Hydrogen] != 4:
            raise TypeError("Methane needs 4 atoms of Hydrogen")
        if not Carbon in d.keys() or d[Carbon] != 1:
            raise TypeError("Methane needs 1 atom of Carbon")

class Dioxygen(Molecule):
    def __init__(self, name = "Dioxygen", atoms = [Oxygen(), Oxygen()]):
        super().__init__(name, atoms)
        d = self.composition()
        if len(d) != 1:
            raise TypeError("Bad atoms in dioxigen")
        if not Oxygen in d.keys() or d[Oxygen] != 2:
            raise TypeError("Dioxygen needs 2 atoms of Oxygen")

class CarbonDioxide(Molecule):
    def __init__(self, name = "CarbonDioxide", atoms = [Carbon(), Oxygen(), Oxygen()]):
        super().__init__(name, atoms)
        d = self.composition()
        if len(d) != 2:
            raise TypeError("Bad atoms in carbon dioxide")
        if not Oxygen in d.keys() or d[Oxygen] != 2:
            raise TypeError("Carbon dioxide needs 2 atoms of Oxygen")
        if not Carbon in d.keys() or d[Carbon] != 1:
            raise TypeError("Carbon dioxide needs 1 atom of Carbon")

class Water(Molecule):
    def __init__(self, name = "Water", atoms = [Hydrogen(), Hydrogen(), Oxygen()]):
        super().__init__(name, atoms)
        d = self.composition()
        if len(d) != 2:
            raise TypeError("Bad atoms in water")
        if not Hydrogen in d.keys() or d[Hydrogen] != 2:
            raise TypeError("Water needs 2 atoms of Hydrogen")
        if not Oxygen in d.keys() or d[Oxygen] != 1:
            raise TypeError("Water needs 1 atom of Oxygen")

Chemical reaction

We'll model a chemical reaction as a ChemicalReaction function.

  • This function takes in a list of molecules.
  • It searches this list to find molecules that can cause a chemical reaction (we'll limit ourselves to the chemical reaction of combustion mentioned at the beginning of this topic).
  • If these molecules are found, it removes these molecules from the list, runs the reaction and returns the list of molecules produced by the reaction. Otherwise, it returns None. The returned molecules are created from the atoms of the molecules at the origin of the chemical reaction.

We know how to create the molecules produced by the chemical reaction, thanks to the constructors, but we currently have no way of recovering the atoms of the molecules at the origin of the chemical reaction.

  • Add to the Molecule class a destroy() method that removes the atoms from a molecule and returns a list containing the removed atoms.

ANSWER ELEMENTS

# Method added in class Molecule
    def destroy(self):
        atoms = self.__atoms
        self.__atoms = []
        return atoms

def ChemicalReaction(molecules):
    def count_molecules(kind):
        return sum([1 for m in molecules if isinstance(m, kind)])

    def remove_molecule(kind):
        for i in range(0, len(molecules)):
            if isinstance(molecules[i], kind):
                return molecules.pop(i)
        return None

    def remove_atom(atoms, kind):
        for i in range(0, len(atoms)):
            if isinstance(atoms[i], kind):
                return atoms.pop(i)
        return None


    dioxygen_count = count_molecules(Dioxygen)
    methane_count = count_molecules(Methane)

    if methane_count >= 1 and dioxygen_count >= 2:
        needed_molecules = [remove_molecule(Methane), remove_molecule(Dioxygen), remove_molecule(Dioxygen)]
        all_atoms = sum([m.destroy() for m in needed_molecules], [])
        return [
            Water        ("w1", [remove_atom(all_atoms, Hydrogen), remove_atom(all_atoms, Hydrogen), remove_atom(all_atoms, Oxygen)]),
            Water        ("w2", [remove_atom(all_atoms, Hydrogen), remove_atom(all_atoms, Hydrogen), remove_atom(all_atoms, Oxygen)]),
            CarbonDioxide("c",  [remove_atom(all_atoms, Carbon),   remove_atom(all_atoms, Oxygen),   remove_atom(all_atoms, Oxygen)])
        ]
    return None