Molècules - Kekule

Kekule és una biblioteca de JavaScript de codi obert per a quimioinformàtica centrada en representar, dibuixar, editar, comparar i cercar estructures de molècules

Dibuixant molecules

Les molécules tenen una estructura tridimensional i es poden representar en 3D.

Ehtanol Panel

Per exemple, a PubChem pots veure l’alcohol etílic ( o etanol) en 3D: Ethanol

També pots veure una representació de la molécula a Molview: Ethanol

Les molècules petites sovint són gràfics planars i es poden dibuixar com una representació bidimensional, amb algunes notacions especials per gestionar la quiralitat.

H H
| |
H-C-C-O-H
| |
H H

Aquesta és una representació molt detallada amb tots aquells hidrògens que s’enganxen als àtoms pesats.

Es pot compactar una mica movent les H al costat de l’àtom pesat:

CH<sub>3</sub>-CH<sub>2</sub>-OH

Els àtoms tenen valències: el carboni té una valència de 4, el que significa que necessita 4 enllaços simples, o 1 enllaç doble i 2 enllaços simples, o 2 enllaços dobles, o 1 enllaç triple i 1 enllaç simple.

Com que escriure tants hidrògens és molt pesat, els químics assumeixen que les valències sempre s’ompliran, en lloc d’enumerar els recomptes d’hidrogen de manera explícita, van decidir utilitzar una representació implícita d’hidrogen, on el nombre d’hidrògens en un àtom és la valència de l’àtom més la càrrega menys la suma dels seus ordres d’enllaç.

A continuació tens un alcohol etílic dibuixat amb hidrògens implícits:

C-C-O

A diferència dels químics generals els químics orgànics també han d’escriure moltes C.

Suposo que ja saps per quin motiu, si no ja pots tornar al principi de l’activitat!

La pràctica estàndard és que els carbonis “normals” no es dibuixen en absolut. Qualsevol final d’enllaç o flexió sense símbol d’element s’assumeix que és un carboni sense càrrega de pes molecular mitjà.

Aquí tens un alcohol etílic tal com ho dibuixaria un químic:

--O

Tot això perquè t’interessa?

  1. Si mires l’estructura 2D de l’etanol a PubChem no pensarás que algún error ha d’haver-hi: Etanol

  2. Que en química orgànica les molècules estan plenes de H i C.


Kekule

Kekule.js és una biblioteca JavaScript de codi obert per a quimioinformàtica.

Actualment, està centrat en les molècules, centrant-se en proporcionar la capacitat de representar, dibuixar, editar, comparar i cercar estructures de molècules als navegadors web.

Tota la biblioteca està dividida en diversos mòduls, cadascun ofereix diferents característiques i funcions. Els usuaris poden utilitzar la combinació de mòduls per a un propòsit específic.

La taula següent enumera les característiques principals de cada mòdul a Kekule.js que ens interessa:

MòdulCaracterísitques principals
CoreRepresentació de conceptes químics com element, àtom, enllaç, molècula i reacció
IOLlegir/escriure diferents formats de dades químiques. Aixó inclou: Format Kekule JSON/XML, CML, MDL MOL2000/3000, SMILES
RenderProporciona mètodes de renderització de navegador creuat de baix nivell per dibuixar molècules (i altres objectes químics) en el context del navegador web

Dibuixar mol·lècules i àtoms amb Kekule

Els àtoms i els enllaços són la base de diverses molècules.

Estan representats com objectes a Kekule.js.

Crea un projecte kekule tal com s’explica a “/ts/module/” :

Terminal window
$ mkdir kekule && cd kekule

Afegeix la llibreria kekule:

Terminal window
$ npm add kekule

Crea un fitxer index.html:

<!doctype html>
<html>
<head>
<title>Kekule</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href=" https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css">
<script type="module" src="index.js"></script>
</head>
<body>
<div id="canvas"/>
</body>
</html>

Arrenca un servidor http:

Terminal window
$ npm install http-server --save-dev
$ node_modules/http-server/bin/http-server -p 8000 -c1
...

A continuació creem el fitxer index.js:

import { Kekule } from './node_modules/kekule/dist/kekule.esm.mjs'
const mol = new Kekule.Molecule();
const a1 = (new Kekule.Atom()).setSymbol('C')
mol.appendNode(a1);
draw(mol)
function draw(mol) {
const drawBoxWidth = 500
const drawBoxHeight = 500
const condensed = true
const canvas = document.getElementById('canvas')
var bridge = new Kekule.Render.CanvasRendererBridge()
var context = bridge.createContext(canvas, drawBoxWidth, drawBoxHeight)
var baseCoord = { 'x': drawBoxWidth / 2, 'y': drawBoxHeight / 2 }
var options = {
moleculeDisplayType: condensed ? Kekule.Render.MoleculeDisplayType.CONDENSED : Kekule.Render.MoleculeDisplayType.SKELETAL,
retainAspect: true,
autoScale: true,
zoom: 4
}
bridge.clearContext(context)
var painter = new Kekule.Render.ChemObjPainter(Kekule.CoordMode.COORD2D, mol, bridge)
painter.draw(context, baseCoord, options)
}

Aquest fitxer té una funció draw que s’ocupa de renderitzar la mólecula.

Obre un navegador a http://localhost:8000/:

El codi que crea la molécula és aquest:

const mol = new Kekule.Molecule();
const a1 = (new Kekule.Atom()).setSymbol('C')
mol.appendNode(a1);

Llavors com és que apareixen 4 H (hidrogen) ? Suposo que ja saps la resposta (s’ha explicat abans).

Pots provar amb un O (Oxigen) a veure que passa (i pensa perqué passa)

També amb una He (Heli) a veure que passa (i pensa perqué passa).

Pots modificar les propietats de l’àtom:

PropertyValueTypeDescripció
atomicNumberintNombre atòmic de l’àtom, p. ex. 6 per al carboni. Es canviarà automàticament quan la propietat symbolestà establert.
symbolstringSímbol de l’àtom, per exemple, “C” per carboni. Es canviarà automàticament quan la propietat atomicNumberestà establert.
massNumberintNombre de massa de l’isòtop.
isotopeIdstringUna propietat de drecera per establir el nombre atòmic i la massa nombre tots alhora, per exemple, ‘13C’ o ‘C13’.
chargefloatCàrrega en aquest àtom. Es permet la càrrega parcial (per exemple, +0,5).
const mol = new Kekule.Molecule();
const a1 = (new Kekule.Atom()).setSymbol('C')
// set atom to represent C13
a1.setSymbol('C').setMassNumber(13); // note that the property can be set cascadedly
// same effect as the previous line
a1.setIsotopeId('C13');
// atom O2-
a1.setAtomicNumber(8).setCharge(-2);
mol.appendNode(a1);

Alguns valors de propietats es poden establir durant la creació de l’àtom:

var atom = new Kekule.Atom('atom1', 'C', 13); // create C13 atom

Enllaç

Per crear un vincle:

var bond = new Kekule.Bond('bond1'); // create a bond with an id

Dues de les propietats més importants de l’enllaç són el seu tipus i ordre:

PropertyValueTypeDescripció
bondTypestringTipus de vincle, es pot configurar a:
Kekule.BondType.COVALENT/IONIC/COORDINATE/METALLIC/HYDROGEN
bondOrderintL’ordre de l’enllaç de covalència es pot establir a:
kekule.BondOrder.SINGLE/DOUBLE/TRIPLE/…
// create a double covalent bond
bond.setBondType(Kekule.BondType.COVALENT).setBondOrder(2);

Si la propietat bondType o bondOrder no està fixada, per defecte es considera que l’enllaç és simple covalent.

Els àtoms es poden connectar entre si mitjançant enllaços, així es crea una molècula.

El codi següent crea una nova molècula d’oxirane amb tres àtoms i tres enllaços:

// create molecule first
const mol = new Kekule.Molecule()
// add three atoms to molecule, property setter can be called cascadely
const a1 = (new Kekule.Atom()).setSymbol('C').setCoord2D({ 'x': -0.4, 'y': 0.23 })
const a2 = (new Kekule.Atom()).setSymbol('C').setCoord2D({ 'x': 0.4, 'y': 0.23 })
const a3 = (new Kekule.Atom()).setSymbol('O').setCoord2D({ 'x': 0, 'y': -0.46 })
mol.appendNode(a1);
mol.appendNode(a2);
mol.appendNode(a3);
// add three bonds to molecule
const b1 = (new Kekule.Bond()).setBondOrder(1).setConnectedObjs([a1, a2])
const b2 = (new Kekule.Bond()).setBondOrder(1).setConnectedObjs([a2, a3])
const b3 = (new Kekule.Bond()).setBondOrder(1).setConnectedObjs([a3, a1])
mol.appendConnector(b1)
mol.appendConnector(b2)
mol.appendConnector(b3)

Els àtoms o enllaços també es poden eliminar fàcilment de la molècula existent:

// remove atom O related bonds in molecule
mol.removeNodeAt(2); // the atom index starts from 0
// or mol.removeNode(mol.getNodeAt(2));
mol.removeConnectorAt(1);
// or mol.removeConnector(mol.getConnectorAt(1));
mol.removeConnectorAt(2);

Molècula després d’eliminar l’àtom O:

[TODO] dibuix

A part dels àtoms normals, també s’admeten pseudoàtoms i àtoms variables:

const mol = new Kekule.Molecule();
// add atoms to molecule
mol.appendNode(new Kekule.Atom().setSymbol('C').setCoord2D({x: 0, y: 0.80}));
// explicit set mass number of an atom
mol.appendNode(new Kekule.Atom().setSymbol('C').setMassNumber(13).setCoord2D({x: -0.69, y: 0.40}));
mol.appendNode(new Kekule.Atom().setSymbol('C').setCoord2D({x: -0.69, y: -0.40}));
// a pseudo atom
mol.appendNode(new Kekule.Pseudoatom().setAtomType(Kekule.PseudoatomType.ANY).setCoord2D({x: 0, y: -0.80}));
mol.appendNode(new Kekule.Atom().setSymbol('C').setCoord2D({x: 0.69, y: -0.40}));
mol.appendNode(new Kekule.Atom().setSymbol('C').setCoord2D({x: 0.69, y: 0.40}));
// a variable atom
mol.appendNode(new Kekule.VariableAtom().setAllowedIsotopeIds(['F', 'Cl', 'Br']).setCoord2D({x: 1.39, y: 0.80}));
// add bonds to molecule
// here a shortcut method appendBond(atomIndexes, bondOrder) is used
mol.appendBond([0, 1], 1);
mol.appendBond([1, 2], 2);
mol.appendBond([2, 3], 1);
mol.appendBond([3, 4], 2);
mol.appendBond([4, 5], 1);
mol.appendBond([5, 0], 2);
mol.appendBond([5, 6], 1);

Fins i tot el subgrup:

const mol = new Kekule.Molecule()
// add atoms to molecule
mol.appendNode(new Kekule.Atom().setSymbol('C').setCoord2D({x: 0, y: 0.80}));
mol.appendNode(new Kekule.Atom().setSymbol('C').setCoord2D({x: -0.69, y: 0.40}));
mol.appendNode(new Kekule.Atom().setSymbol('C').setCoord2D({x: -0.69, y: -0.40}));
mol.appendNode(new Kekule.Atom().setSymbol('C').setCoord2D({x: 0, y: -0.80}));
mol.appendNode(new Kekule.Atom().setSymbol('C').setCoord2D({x: 0.69, y: -0.40}));
mol.appendNode(new Kekule.Atom().setSymbol('C').setCoord2D({x: 0.69, y: 0.40}));
// create a sub group
const sub = new Kekule.SubGroup();
// add atoms/bonds to sub structure
sub.appendNode(new Kekule.Atom().setSymbol('C').setCoord2D({x: 1.39, y: 0.80}));
sub.appendNode(new Kekule.Atom().setSymbol('C').setCoord2D({x: 2.0, y: 0.40}));
sub.appendBond([0, 1], 1);
// atom 0 in subgroup connected with main body
sub.appendAnchorNode(sub.getNodeAt(0));
sub.setAbbr('Et');
// then add sub structure to molecule
mol.appendNode(sub);
// add bonds to molecule
// here a shortcut method appendBond(atomIndexes, bondOrder) is used
mol.appendBond([0, 1], 1);
mol.appendBond([1, 2], 2);
mol.appendBond([2, 3], 1);
mol.appendBond([3, 4], 2);
mol.appendBond([4, 5], 1);
mol.appendBond([5, 0], 2);
mol.appendBond([5, 6], 1); // bond connecting subgroup

Enllaços multicèntrics

El conjunt d’eines admet enllaços multicèntrics (per exemple, enllaç B-H-B en diborà i enllaç Cp-Fe en ferrocè).

Aquest enllaç es pot crear assignant múltiples àtoms connectats:

// create molecule
const mol = new Kekule.Molecule();
// add atoms to molecule
mol.appendNode(new Kekule.Atom().setSymbol('B').setExplicitHydrogenCount(2).setCoord2D({x: -1, y: 0}));
mol.appendNode(new Kekule.Atom().setSymbol('B').setExplicitHydrogenCount(2).setCoord2D({x: 1, y: 0}));
mol.appendNode(new Kekule.Atom().setSymbol('H').setCoord2D({x: 0, y: 1}));
mol.appendNode(new Kekule.Atom().setSymbol('H').setCoord2D({x: 0, y: -1}));
// add two multicenter bond: B-H-B
mol.appendBond([0, 2, 1], 1);
mol.appendBond([0, 3, 1], 1);

Connexió enllaç-enllaç

També s’admet una connexió especial d’enllaç (per exemple, a la sal de Zeise):

// create molecule
var mol = new Kekule.Molecule();
var atomPt = new Kekule.Atom(); // Pt atom
mol.appendNode(atomPt.setSymbol('Pt').setCoord2D({x: 0.35, y: 0}));
mol.appendNode(new Kekule.Atom().setSymbol('Cl').setCoord2D({x: 0.35, y: 0.80}));
mol.appendNode(new Kekule.Atom().setSymbol('Cl').setCoord2D({x: 0.35, y: -0.80}));
mol.appendNode(new Kekule.Atom().setSymbol('Cl').setCoord2D({x: 1.14, y: 0}));
mol.appendNode(new Kekule.Atom().setSymbol('C').setCoord2D({x: -0.45, y: 0.40}));
mol.appendNode(new Kekule.Atom().setSymbol('C').setCoord2D({x: -0.45, y: -0.40}));
mol.appendBond([1, 0], 1, Kekule.BondType.IONIC); // Pt-Cl
mol.appendBond([2, 0], 1, Kekule.BondType.IONIC); // Pt-Cl
mol.appendBond([3, 0], 1, Kekule.BondType.IONIC); // Pt-Cl
var doubleBond = mol.appendBond([4, 5], 2); // C=C
// create bond-bond connection
var coordinateBond = new Kekule.Bond(); // (C=C)-Pt
coordinateBond.setBondType(Kekule.BondType.COORDINATE);
coordinateBond.setConnectedObjs([doubleBond, atomPt]);
mol.appendConnector(coordinateBond);

Fitxers

És molt poc habitual crear una molècula mitjançant codis JavaScript purs.

Normalment l’usuari carrega una molècula de dades externes (per exemple, un fitxer o un string)

El mòdul Kekule.IO proporciona una sèrie de mètodes per realitzar l’entrada i sortida de molècules (i altres objectes químics).

Guardar una mòlecula

Per guardar una molècula, només cal utilitzar saveFormatData:

var data = Kekule.IO.saveFormatData(mol, 'mol');
// or Kekule.IO.saveMimeData(mol, 'chemical/x-mdl-molfile');
console.log(data);
var data = Kekule.IO.saveFormatData(mol, 'smi');
// or Kekule.IO.saveMimeData(mol, 'chemical/x-daylight-smiles');
console.log(data);

Les dades de retorn solen ser un string.

Carregant desde string

Si la font de dades és un string el procés de càrrega és bastant senzill amb Kekule.IO.loadFormatData:

var cmlData = `<cml xmlns="http://www.xml-cml.org/schema">
<molecule id="m1">
<atomArray>
<atom id="a2" elementType="C" x2="7.493264658965051" y2="35.58088907877604"/>
<atom id="a3" elementType="O" x2="8.186084981992602" y2="35.18088907877604"/>
<atom id="a1" elementType="C" x2="6.800444335937501" y2="35.18088907877604"/>
</atomArray>
<bondArray>
<bond id="b2" order="S" atomRefs2="a2 a3"/>
<bond id="b1" order="S" atomRefs2="a2 a1"/>
</bondArray>
</molecule>
</cml>`;
var mol = Kekule.IO.loadFormatData(cmlData, 'cml');
// or Kekule.IO.loadMimeData(cmlData, 'chemical/x-cml');

La molècula llegida de la cadena es retornarà immediatament. Per exemple, la molècula d’etanol carregada a l’exemple anterior:

Carregant des del fitxer

És una sol·licitud habitual per carregar molècules des del fitxer font.

El conjunt d’eines proporciona dos mètodes per a aquesta tasca: Kekule.IO.loadUrlData i Kekule.IO.loadFileData.

loadUrlData

El mètode loadUrlData obté dades d’un URL de fitxer remot, ja sigui una ruta absoluta (com ara http://www.mysite.com/myMolecule.cml ) o un camí relatiu (com data/myMolecule.cml ). El mètode carrega el fitxer font mitjançant AJAX , així que comparteix les mateixes limitacions amb AJAX (per exemple, same-origin policy.

Com que la transferència de dades a través de la xarxa pot costar molt de temps, loadUrlData utilitzar el model asíncron per retornar el resultat. Accepta dos paràmetres, el primer és l’URL de la font, la segona és una funció de devolució de trucada que es cridarà quan finalitzi el procés de càrrega. L’usuari hauria de comprovar la molècula carregada en aquesta funció de devolució de trucada:

var url = 'data/mol2D/quinone.mol';
Kekule.IO.loadUrlData(url, function(mol, success){
if (success)
{
console.log('Loading from ' + url + ' Successful');
showMolecule(mol);
}
else
{
console.log('Loading from ' + url + ' Failed');
}
});
loadFileData

En els navegadors web moderns, també es pot carregar el fitxer desde una font local amb Kekule.IO.loadFileData i File API.

Aquest és l’exemple:

document.getElementById('inputFile').addEventListener('change', function()
{
var file = document.getElementById('inputFile').files[0];
if (file)
{
Kekule.IO.loadFileData(file, function(mol, success)
{
if (success && mol)
showMolecule(mol);
});
}
});

loadFileDatatambé utilitza el model asíncron i necessita una callback function.

Draw

Per dibuixar fem servir una classe especial que s’anomena painter.

El següent fragment de codi s’utilitza per dibuixar molècules amb Painter:

var renderType = Kekule.Render.RendererType.R2D//R3D // do 2D or 3D drawing
// parent element, we will draw inside it
var parentElem = document.getElementById('parent');
// clear parent elem
Kekule.DomUtils.clearChildContent(parentElem);
// create painter, bind with molecule
var painter = new Kekule.Render.ChemObjPainter(renderType, mol);
// create context inside parentElem
var dim = Kekule.HtmlElementUtils.getElemOffsetDimension(parentElem); // get width/height of parent element
var context = painter.createContext(parentElem, dim.width, dim.height); // create context fulfill parent element
// at last, draw the molecule at the center of context
painter.draw(context, {'x': dim.width / 2, 'y': dim.height / 2});

De fet, les molècules i altres tipus d’objectes es poden dibuixar a la pàgina HTML d’aquesta manera.

Per exemple, el codi següent dibuixa una fletxa d’equilibri:

// Create arrow glyph with initial parameters
var glyph = new Kekule.Glyph.StraightLine('glyph1', 1, {
'startArrowType': Kekule.Glyph.ArrowType.OPEN,
'startArrowSide': Kekule.Glyph.ArrowSide.REVERSED,
'startArrowWidth': 0.25,
'startArrowLength': 0.25,
'endArrowType': Kekule.Glyph.ArrowType.OPEN,
'endArrowSide': Kekule.Glyph.ArrowSide.SINGLE,
'endArrowWidth': 0.25,
'endArrowLength': 0.25,
'lineLength': 1.5,
'lineGap': 0.1,
'lineCount': 2
});
// create new painter, bind with glyph
var painter = new Kekule.Render.ChemObjPainter(Kekule.Render.RendererType.R2D, glyph);
// draw the glyph at the center of context we previous created
painter.draw(context, {'x': dim.width / 2, 'y': dim.height / 2});

El resultat:

Formats de fitxers

SDF

El format de fitxer de dades d’estructura (SDF) es basa en el format de fitxer MOL, ambdós desenvolupats per MDL Information Systems que després va ser adquirit per Biovia (ara anomenada Biovia, que pertany a Dassault Systems).

El fitxer MOL original només codificava una sola molècula, mentre que els fitxers en format SDF poden codificar una o diverses molècules.

Els fitxers SDF són fitxers ASCII amb format que emmagatzemen informació sobre les posicions dels àtoms individuals que formen la molècula. També es codifica la informació sobre l’estat d’hibridació i la connectivitat, tot i que aquestes últimes dades s’utilitzen amb menys freqüència i sovint de manera inconsistent.

Múltiples molècules estan delimitades per línies que consisteixen en quatre signes de dòlar ($$$$).

En aquest enllaç tens un exemple d’una molécula d’aigua: https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/name/water/SDF

962
-OEChem-06162407452D
3 2 0 0 0 0 0 0 0999 V2000
2.5369 -0.1550 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0
3.0739 0.1550 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
2.0000 0.1550 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0
1 2 1 0 0 0 0
1 3 1 0 0 0 0
M END
> <PUBCHEM_COMPOUND_CID>
962
...

Descàrrega directa de mol·lècules.

Terminal window
$ curl https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/name/adenine/SDF -o adenine.mol

En general, per a mol·lècules que existeixin les podem baixar amb la instrucció:

Terminal window
curl https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/name/{nom_molecula}/SDF -o {nom_molecula}.mol

Per descarregar-vos molècules des de TypeScript o Javascript ho podeu fer amb les classes que proporciona el mòdul Kekule.IO:

var reader = new Kekule.IO.MdlReader();
//var multiple = parseInt(document.getElementById('editMultiple').value);
var mol1;
var result;
var r = reader.readData(data, Kekule.IO.ChemDataType.TEXT);
mol1 = r;
result = r;

Informació addicional des del web de l’NCBI:

El subportal d’NCBI pubchem et permet cercar tota la informació sobre mol·lècules:

PubChem

Quan el trobem, podem veure tota la informació, descarregar-la en diversos formats i fins i tot visualitzar-la amb 2D i 3D. A més a més, et pots descarregar la visualització 2D o 3D.

En el cas de l’Ethanol (l’alcohol), aquestes són les URL que tenim (qualsevol de les 2 val).

El paracetamol: https://pubchem.ncbi.nlm.nih.gov/compound/1983

MOL

Molfile és un format de fitxer basat en text utilitzat per emmagatzemar i transmetre estructures moleculars.

Tot i que els molfiles s’utilitzen més habitualment en química orgànica, el format té èxit en diferents graus a l’hora de representar també molècules inorgàniques i organometàl·liques.


Projecte

Per treballar amb la llibreria farem servir “/ts/react/”

Terminal window
$ git clone https://gitlab.com/xtec/bio-kekule
$ cd bio-kekule
$ ./init.sh

Tanca el terminal i torna’l a obrir:

Terminal window
$ npm run next-dev

Obre el navegador a http://localhost:3000/molecule.

A partir del projecte inicial has de realitzar modificacions per ampliar la seva funcionalitat.

Per exemple:

  1. Opció de renderitzar les molècules en 3D

  2. A part de mostrar la molècula, també una descripció de la molécula que pots renderitzar a partir de wikipedia.

  3. Que l’usuari pugui modificar el fitxer SDF i es dibuixi la nova molècula

  4. Cada cop que es renderitza una molècula enviar el nom al servidor per saber quines són les més buscades.

  5. Poder editar una molècula

En aquest enllaç tens vàries demos a partir de kekule: Kekule.js Demos

Aquí tens un altre exemple: Hack-a-Mol

I segur que en troves d’altres a Internet de visualització i manipulació de molècules.

Al finalitzar l’activitat has de construir una imatge i desplegar-la a Azure.

Informació de la mol.lecula

TODO

Incorporar: Atoms, Molecules, and Ions