Skip to content

Commit 0839bff

Browse files
andyclaude
andcommitted
The symmetry orbitals themselves, not just which irreps appear (#132)
Completes phase 3. The reduction says a1 + t2; the diagram needs the combinations, so this applies the projection operator P = (l/h) sum_R chi(R)* R to each orbital of an orbit and picks the independent results out by Gram-Schmidt. Checked against what a student writes by hand: CH4 A1 (1/2, 1/2, 1/2, 1/2) + T2 (3 vectors) H2O A1 (1/sqrt2, 1/sqrt2) + B1 (1 vector) SF6 A1g (1/sqrt6 x 6) + Eg (2) + T1u (3) and on three invariants that would catch a wrong projector without anyone having to know the answer: the count for each irrep is its multiplicity times its dimension, the vectors together span the orbit with nothing left over, and every one is normalised and orthogonal to the rest. The totally symmetric one is checked to be the in-phase combination -- every coefficient equal in magnitude -- which is the thing a chemist would look at first and the thing a sign error would break. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EFMUNbw2UpRPmgsqviAowb
1 parent 1d57ce6 commit 0839bff

3 files changed

Lines changed: 154 additions & 0 deletions

File tree

‎include/tdat/SymmetryAnalysis.H‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,35 @@ class SymmetryAnalysis
101101
const vector<int>& classOfOp,
102102
int numClasses,
103103
vector<double>& chi);
104+
105+
/**
106+
* The symmetry orbitals of one irrep, over an orbit's s orbitals.
107+
*
108+
* The projection operator, P = (l/h) sum_R chi(R)* R, applied to
109+
* each orbital of the orbit in turn. That gives as many vectors as
110+
* there are orbitals, most of them repeats or zero, so the
111+
* independent ones are picked out by Gram-Schmidt and normalised.
112+
*
113+
* These are the terminal-atom symmetry orbitals -- the in-phase and
114+
* out-of-phase combinations a diagram draws on one side.
115+
*
116+
* @param table the group's character table
117+
* @param irrep which irrep to project onto
118+
* @param vectors filled with one coefficient vector per independent
119+
* symmetry orbital, each over the orbit's atoms in
120+
* orbit order
121+
*
122+
* @return false if the arguments do not describe this group. The
123+
* number of vectors returned is the irrep's multiplicity
124+
* times its dimension, and a caller that knows the expected
125+
* reduction should check it.
126+
*/
127+
static bool projectOrbit(const vector<int>& orbit,
128+
const vector< vector<int> >& images,
129+
const vector<int>& classOfOp,
130+
const CharacterTable& table,
131+
const string& irrep,
132+
vector< vector<double> >& vectors);
104133
};
105134

106135
#endif

‎src/tdat/chemistry/SymmetryAnalysis.C‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
#include "tdat/SymmetryAnalysis.H"
77
#include "tdat/CharacterTable.H"
88

9+
#include <map>
10+
using std::map;
11+
912
namespace {
1013

1114
/** Apply an operation to a point. */
@@ -359,3 +362,65 @@ bool SymmetryAnalysis::orbitalCharacter(const vector<int>& orbit,
359362
for (int c = 0; c < numClasses; c++) if (seen[c] == 0) return false;
360363
return true;
361364
}
365+
366+
367+
bool SymmetryAnalysis::projectOrbit(const vector<int>& orbit,
368+
const vector< vector<int> >& images,
369+
const vector<int>& classOfOp,
370+
const CharacterTable& table,
371+
const string& irrep,
372+
vector< vector<double> >& vectors)
373+
{
374+
vectors.clear();
375+
376+
const vector<double>* chi = table.characters(irrep);
377+
if (chi == 0) return false;
378+
if (classOfOp.size() != images.size()) return false;
379+
380+
const size_t n = orbit.size();
381+
if (n == 0) return false;
382+
383+
// Where each atom sits in the orbit, so an image can be turned back
384+
// into a position in the coefficient vector.
385+
map<int, size_t> position;
386+
for (size_t i = 0; i < n; i++) position[orbit[i]] = i;
387+
388+
const double dimension = table.dimension(irrep);
389+
const double h = table.order();
390+
if (h <= 0.0) return false;
391+
392+
// Project each orbital of the orbit in turn. Most of the results
393+
// are repeats of one another or zero; Gram-Schmidt below keeps the
394+
// independent ones.
395+
for (size_t a = 0; a < n; a++) {
396+
397+
vector<double> v(n, 0.0);
398+
for (size_t o = 0; o < images.size(); o++) {
399+
const int cls = classOfOp[o];
400+
if (cls < 0 || (size_t)cls >= chi->size()) return false;
401+
402+
map<int, size_t>::const_iterator it = position.find(images[o][orbit[a]]);
403+
if (it == position.end()) continue; // left the orbit: cannot happen
404+
v[it->second] += (*chi)[cls];
405+
}
406+
for (size_t i = 0; i < n; i++) v[i] *= dimension/h;
407+
408+
// Remove whatever is already spanned, then keep what is left if
409+
// there is anything of it.
410+
for (size_t k = 0; k < vectors.size(); k++) {
411+
double dot = 0.0;
412+
for (size_t i = 0; i < n; i++) dot += v[i]*vectors[k][i];
413+
for (size_t i = 0; i < n; i++) v[i] -= dot*vectors[k][i];
414+
}
415+
416+
double norm = 0.0;
417+
for (size_t i = 0; i < n; i++) norm += v[i]*v[i];
418+
if (norm < 1.0e-10) continue; // nothing new
419+
420+
norm = sqrt(norm);
421+
for (size_t i = 0; i < n; i++) v[i] /= norm;
422+
vectors.push_back(v);
423+
}
424+
425+
return true;
426+
}

‎tests/symmetry/testSymmetryAnalysis.C‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,66 @@ static void run(const Molecule& mol, const string& opsDir)
103103
check(ok, "conjugacy classes matched to the table's named classes");
104104
if (!ok) return;
105105

106+
// The symmetry orbitals themselves, not just which irreps appear.
107+
for (size_t i = 0; i < orbits.size(); i++) {
108+
if (orbits[i].size() != mol.terminalCount) continue;
109+
110+
vector<double> chi;
111+
if (!SymmetryAnalysis::orbitalCharacter(orbits[i], images, classOfOp,
112+
(int)table->classes().size(), chi))
113+
continue;
114+
vector<int> mult;
115+
if (!table->reduce(chi, mult)) continue;
116+
117+
size_t total = 0;
118+
for (size_t k = 0; k < table->irreps().size(); k++) {
119+
if (mult[k] == 0) continue;
120+
const string& ir = table->irreps()[k];
121+
122+
vector< vector<double> > vecs;
123+
bool got = SymmetryAnalysis::projectOrbit(orbits[i], images, classOfOp,
124+
*table, ir, vecs);
125+
snprintf(msg, sizeof(msg),
126+
"%s: %zu symmetry orbitals (%d x dim %d = %d expected)",
127+
ir.c_str(), vecs.size(), mult[k], table->dimension(ir),
128+
mult[k]*table->dimension(ir));
129+
check(got && (int)vecs.size() == mult[k]*table->dimension(ir), msg);
130+
total += vecs.size();
131+
132+
// Each must be normalised and orthogonal to the others.
133+
for (size_t u = 0; u < vecs.size(); u++) {
134+
double nrm = 0.0;
135+
for (size_t c = 0; c < vecs[u].size(); c++) nrm += vecs[u][c]*vecs[u][c];
136+
if (fabs(nrm - 1.0) > 1e-9) {
137+
check(false, string(ir) + ": a symmetry orbital is not normalised");
138+
break;
139+
}
140+
}
141+
}
142+
// Together they must span the whole orbit -- no orbital left over.
143+
snprintf(msg, sizeof(msg),
144+
"the symmetry orbitals span the orbit (%zu of %zu)",
145+
total, orbits[i].size());
146+
check(total == orbits[i].size(), msg);
147+
148+
// The totally symmetric one is the in-phase combination: every
149+
// coefficient equal and positive. That is the check a student
150+
// would do by eye.
151+
const string& first = table->irreps()[0];
152+
vector< vector<double> > sym;
153+
if (SymmetryAnalysis::projectOrbit(orbits[i], images, classOfOp,
154+
*table, first, sym) && sym.size() == 1) {
155+
const double want = 1.0/sqrt((double)orbits[i].size());
156+
bool inPhase = true;
157+
for (size_t c = 0; c < sym[0].size(); c++)
158+
if (fabs(fabs(sym[0][c]) - want) > 1e-9) inPhase = false;
159+
snprintf(msg, sizeof(msg),
160+
"%s is the in-phase combination, all coefficients %.4f",
161+
first.c_str(), want);
162+
check(inPhase, msg);
163+
}
164+
}
165+
106166
// The terminal orbit's s orbitals: the reduction a textbook gives.
107167
for (size_t i = 0; i < orbits.size(); i++) {
108168
if (orbits[i].size() != mol.terminalCount) continue;

0 commit comments

Comments
 (0)