Skip to content

Commit 2cbbd78

Browse files
authored
Merge pull request #2587 from ghutchis/fix-unit-cell-formulas
Fix unit cell formulas
2 parents 9a1e014 + 1a35f9d commit 2cbbd78

File tree

4 files changed

+325
-34
lines changed

4 files changed

+325
-34
lines changed

avogadro/core/molecule.cpp

Lines changed: 145 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
#include <algorithm>
2121
#include <cassert>
22+
#include <cmath>
2223
#include <cstddef>
2324
#include <iostream>
2425
#include <utility>
@@ -953,35 +954,48 @@ const Cube* Molecule::activeCube() const
953954

954955
std::string Molecule::formula(const std::string& delimiter, int over) const
955956
{
956-
// Adapted from chemkit:
957-
// A map of atomic symbols to their quantity.
958-
std::map<unsigned char, size_t> componentsCount = composition();
957+
// A map of element symbols (including isotopes) to their quantity.
958+
std::map<std::string, size_t> componentsCount = formulaComposition();
959959

960960
std::stringstream result;
961-
std::map<unsigned char, size_t>::iterator iter;
961+
std::map<std::string, size_t>::iterator iter;
962962

963963
// Carbons first
964-
iter = componentsCount.find(6);
964+
iter = componentsCount.find("C");
965965
if (iter != componentsCount.end()) {
966966
result << "C";
967967
if (iter->second > static_cast<size_t>(over))
968968
result << delimiter << iter->second;
969969
componentsCount.erase(iter);
970970

971-
// If carbon is present, hydrogens are next.
972-
iter = componentsCount.find(1);
971+
// If carbon is present, hydrogens are next (including D and T).
972+
iter = componentsCount.find("H");
973973
if (iter != componentsCount.end()) {
974974
result << delimiter << "H";
975975
if (iter->second > static_cast<size_t>(over))
976976
result << delimiter << iter->second;
977977
componentsCount.erase(iter);
978978
}
979+
iter = componentsCount.find("D");
980+
if (iter != componentsCount.end()) {
981+
result << delimiter << "D";
982+
if (iter->second > static_cast<size_t>(over))
983+
result << delimiter << iter->second;
984+
componentsCount.erase(iter);
985+
}
986+
iter = componentsCount.find("T");
987+
if (iter != componentsCount.end()) {
988+
result << delimiter << "T";
989+
if (iter->second > static_cast<size_t>(over))
990+
result << delimiter << iter->second;
991+
componentsCount.erase(iter);
992+
}
979993
}
980994

981-
// The rest:
995+
// The rest (alphabetically, since std::map<std::string> sorts by key):
982996
iter = componentsCount.begin();
983997
while (iter != componentsCount.end()) {
984-
result << delimiter << Elements::symbol(iter->first);
998+
result << delimiter << iter->first;
985999
if (iter->second > static_cast<size_t>(over))
9861000
result << delimiter << iter->second;
9871001
++iter;
@@ -1555,11 +1569,130 @@ bool Molecule::hasCustomElements() const
15551569

15561570
std::map<unsigned char, size_t> Molecule::composition() const
15571571
{
1558-
// A map of atomic symbols to their quantity.
1572+
const double tolerance = 1.0e-3;
1573+
1574+
// A map of atomic numbers to their quantity (using double for fractional
1575+
// contributions when there's a unit cell)
1576+
std::map<unsigned char, double> compositionDouble;
1577+
1578+
// Check if we have a unit cell - if so, we need to account for fractional
1579+
// atoms at corners, edges, and faces
1580+
if (m_unitCell != nullptr) {
1581+
for (Index i = 0; i < atomCount(); ++i) {
1582+
unsigned char atomicNum = m_atomicNumbers[i];
1583+
Vector3 fracCoords = m_unitCell->toFractional(m_positions3d[i]);
1584+
1585+
// Count how many coordinates are at boundaries (0 or 1)
1586+
int boundaryCount = 0;
1587+
for (int j = 0; j < 3; ++j) {
1588+
double coord = fracCoords[j];
1589+
// Check if close to 0 or 1
1590+
if (std::fabs(coord) < tolerance ||
1591+
std::fabs(coord - 1.0) < tolerance) {
1592+
++boundaryCount;
1593+
}
1594+
}
1595+
1596+
// Calculate fractional contribution based on boundary count
1597+
// Corner atoms (3 boundaries): 1/8
1598+
// Edge atoms (2 boundaries): 1/4
1599+
// Face atoms (1 boundary): 1/2
1600+
// Interior atoms (0 boundaries): 1
1601+
double weight = 1.0;
1602+
if (boundaryCount == 3) {
1603+
weight = 1.0 / 8.0;
1604+
} else if (boundaryCount == 2) {
1605+
weight = 1.0 / 4.0;
1606+
} else if (boundaryCount == 1) {
1607+
weight = 1.0 / 2.0;
1608+
}
1609+
1610+
compositionDouble[atomicNum] += weight;
1611+
}
1612+
} else {
1613+
// No unit cell, just count atoms normally
1614+
for (unsigned char atomicNum : m_atomicNumbers) {
1615+
compositionDouble[atomicNum] += 1.0;
1616+
}
1617+
}
1618+
1619+
// Convert to size_t by rounding to nearest integer
15591620
std::map<unsigned char, size_t> composition;
1560-
for (unsigned char m_atomicNumber : m_atomicNumbers) {
1561-
++composition[m_atomicNumber];
1621+
for (const auto& pair : compositionDouble) {
1622+
size_t roundedCount = static_cast<size_t>(std::round(pair.second));
1623+
if (roundedCount > 0) {
1624+
composition[pair.first] = roundedCount;
1625+
}
15621626
}
1627+
1628+
return composition;
1629+
}
1630+
1631+
std::map<std::string, size_t> Molecule::formulaComposition() const
1632+
{
1633+
const double tolerance = 1.0e-3;
1634+
1635+
// A map of element symbols (with isotopes) to their quantity
1636+
// Using double to accumulate fractional contributions from unit cells
1637+
std::map<std::string, double> compositionDouble;
1638+
1639+
for (Index i = 0; i < atomCount(); ++i) {
1640+
unsigned char atomicNum = m_atomicNumbers[i];
1641+
std::string atomSymbol(Elements::symbol(atomicNum));
1642+
1643+
// Handle isotopes
1644+
unsigned short iso = isotope(i);
1645+
if (iso > 0) {
1646+
if (atomicNum == 1 && iso == 1)
1647+
atomSymbol = "H";
1648+
else if (atomicNum == 1 && iso == 2)
1649+
atomSymbol = "D";
1650+
else if (atomicNum == 1 && iso == 3)
1651+
atomSymbol = "T";
1652+
else
1653+
// e.g., 13C
1654+
atomSymbol = std::to_string(iso) + atomSymbol;
1655+
}
1656+
1657+
// Calculate fractional contribution for unit cells
1658+
double weight = 1.0;
1659+
if (m_unitCell != nullptr) {
1660+
Vector3 fracCoords = m_unitCell->toFractional(m_positions3d[i]);
1661+
1662+
// Count how many coordinates are at boundaries (0 or 1)
1663+
int boundaryCount = 0;
1664+
for (int j = 0; j < 3; ++j) {
1665+
double coord = fracCoords[j];
1666+
if (std::fabs(coord) < tolerance ||
1667+
std::fabs(coord - 1.0) < tolerance) {
1668+
++boundaryCount;
1669+
}
1670+
}
1671+
1672+
// Corner atoms (3 boundaries): 1/8
1673+
// Edge atoms (2 boundaries): 1/4
1674+
// Face atoms (1 boundary): 1/2
1675+
if (boundaryCount == 3) {
1676+
weight = 1.0 / 8.0;
1677+
} else if (boundaryCount == 2) {
1678+
weight = 1.0 / 4.0;
1679+
} else if (boundaryCount == 1) {
1680+
weight = 1.0 / 2.0;
1681+
}
1682+
}
1683+
1684+
compositionDouble[atomSymbol] += weight;
1685+
}
1686+
1687+
// Convert to size_t by rounding to nearest integer
1688+
std::map<std::string, size_t> composition;
1689+
for (const auto& pair : compositionDouble) {
1690+
size_t roundedCount = static_cast<size_t>(std::round(pair.second));
1691+
if (roundedCount > 0) {
1692+
composition[pair.first] = roundedCount;
1693+
}
1694+
}
1695+
15631696
return composition;
15641697
}
15651698

avogadro/core/molecule.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,13 @@ class AVOGADROCORE_EXPORT Molecule
896896
*/
897897
std::map<unsigned char, size_t> composition() const;
898898

899+
/**
900+
* @return a map of element symbols (including isotopes like D, T, 13C) to
901+
* their count. Handles unit cell fractional contributions for atoms at
902+
* corners (1/8), edges (1/4), and faces (1/2).
903+
*/
904+
std::map<std::string, size_t> formulaComposition() const;
905+
899906
/**
900907
* @return the atom pairs for all bonds to the atom indexed at @a index.
901908
*/

avogadro/qtgui/molecule.cpp

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -328,28 +328,8 @@ RWMolecule* Molecule::undoMolecule()
328328

329329
QString Molecule::formattedFormula() const
330330
{
331-
// we're re-implmenting it here to enable isotopes
332-
std::map<std::string, size_t> componentsCount;
333-
334-
// loop through the atoms
335-
for (Index i = 0; i < atomCount(); ++i) {
336-
unsigned short atNumber = atomicNumber(i);
337-
std::string atomSymbol(Core::Elements::symbol(atNumber));
338-
unsigned short iso = isotope(i);
339-
if (iso > 0) {
340-
if (atNumber == 1 && iso == 1)
341-
atomSymbol = "H";
342-
else if (atNumber == 1 && iso == 2)
343-
atomSymbol = "D";
344-
else if (atNumber == 1 && iso == 3)
345-
atomSymbol = "T";
346-
else
347-
// eg. 13C
348-
atomSymbol = std::to_string(iso) + atomSymbol;
349-
}
350-
351-
componentsCount[atomSymbol]++;
352-
}
331+
// Get composition from core (handles isotopes and unit cell fractions)
332+
std::map<std::string, size_t> componentsCount = formulaComposition();
353333

354334
QString formula;
355335
// loop through the components

0 commit comments

Comments
 (0)