Skip to content

Commit e5f15df

Browse files
andyclaude
andcommitted
Scene scripts: nmcheck and mopanel, to measure #228 and #226 headlessly
nmcheck <name> writes, per normal-mode arrow, the farthest arrow vertex from the atom centre (SoCallbackAction triangles) and where a ray along the arrow meets that atom's sphere (SoRayPickAction, atom detail), so "the arrow ends inside its atom" is read off the drawn scene rather than from NModeVectCmd's arithmetic. On calc-water-vib the O arrow tip is 0.038 A (mode 1) / 0.053 A (mode 2) from the centre against a 0.365 A sphere. mopanel <name> (Builder only, through a SceneScript extension) presses the MO panel's Compute and records every frame the Builder canvas paints, with the thresholds of the ChemIso nodes in the scene at that frame. ChemIso::regenerate logs each surface it generates under ECCE_DEBUG_ISO. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PFPK14wBcLD1s4dYUowpP4
1 parent b9441a4 commit e5f15df

7 files changed

Lines changed: 237 additions & 1 deletion

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
type: map
3+
title: "Normal-mode arrows start at the atom centre and scale uniformly; small ones end inside the sphere (#228)"
4+
area: wx-viewer
5+
paths: [src/viz/propsgcommands/NModeVectCmd.C, src/viz/nodes/VRVector.C, src/apps/builder/NModePanel.C, src/wxviz/viewer/SceneScript.C]
6+
issues: [228]
7+
---
8+
`NModeVectCmd` puts one `VRVector` per atom at the atom centre, direction
9+
`d * 0.5/maxnorm` (Scale slider multiplies it). `VRVector` scales its whole
10+
geometry (shaft, head, thickness) by |d|; the tip is at 1.075 |d|, so the
11+
longest arrow reaches 0.5375 A from its centre.
12+
13+
Displayed sphere radii (Ball And Stick and the default style) are covalent
14+
x 0.5: O 0.365, H 0.20, C about 0.38. An atom whose displacement is below
15+
about 70 % of the largest one (any heavy atom in an X-H stretch) has its
16+
whole arrow inside its sphere. Separately, in Ball And Stick an arrow
17+
pointing along a bond lies inside the bond cylinder and is hidden too (the
18+
water symmetric stretch shows no arrows at all with Sign unticked).
19+
20+
Measured, not computed: the scene command `nmcheck <name>` reports per atom
21+
the farthest arrow vertex from the atom centre (`SoCallbackAction`
22+
triangles) and where a ray along the arrow meets that atom's sphere
23+
(`SoRayPickAction`, atom detail); `tools/coin/scenes/calc-water-nmcheck.scene`.

‎include/wxviz/SceneScript.H‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#ifndef SCENESCRIPT_H_
22
#define SCENESCRIPT_H_
33

4+
#include <functional>
45
#include <string>
56
#include <vector>
67
using std::string;
@@ -9,6 +10,7 @@ using std::vector;
910
class IPropCalculation;
1011
class SGContainer;
1112
class SGViewer;
13+
class wxWindow;
1214

1315
/**
1416
* Drives the viewer from a text script and writes offscreen snapshots, so
@@ -32,6 +34,11 @@ class SGViewer;
3234
* viewall | clear reset the camera / hide MO and modes
3335
* snap <name> the canvas, read back with glReadPixels -> <name>.ppm
3436
* thumb <name> 64x64 black background, as the calculation thumbnail
37+
* nmcheck <name> per normal-mode arrow: tip distance from its atom centre
38+
* (arrow triangles) and the atom's sphere surface along
39+
* the arrow (ray pick) -> <name>.txt
40+
* Commands this class does not know go to the caller's extension, if set
41+
* (the Builder adds "mopanel", which presses the MO panel's Compute).
3542
*/
3643
class SceneScript
3744
{
@@ -43,20 +50,32 @@ class SceneScript
4350
bool run(const string& scriptPath);
4451
const string& message() const { return p_msg; }
4552

53+
// Unknown commands go here; return false (and set fail()) on error.
54+
typedef std::function<bool(SceneScript&, const vector<string>&)> Extension;
55+
void setExtension(Extension e) { p_ext = e; }
56+
57+
// Runs act() and records every frame the render area under canvasOwner
58+
// paints meanwhile: <name>-NN.ppm plus one line per frame in
59+
// <name>-frames.txt with the thresholds of the isosurfaces drawn.
60+
bool recordFrames(wxWindow *canvasOwner, const string& name,
61+
std::function<void()> act);
62+
bool fail(const string& m) { p_msg = m; return false; }
63+
4664
private:
4765
bool exec(const vector<string>& w, const string& rest);
4866
bool pickAtoms(const string& name, const vector<string>& atoms);
4967
bool dragAtom(const string& name, int atom, int dx, int dy);
5068
bool countRedraws(const string& name, const vector<string>& steps);
5169
bool snapshot(const string& name, int size, float r, float g, float b);
5270
bool exportIso(const string& name);
53-
bool fail(const string& m) { p_msg = m; return false; }
71+
bool nmCheck(const string& name);
5472

5573
SGViewer *p_viewer;
5674
SGContainer *p_sg;
5775
IPropCalculation *p_calc;
5876
string p_outdir;
5977
string p_msg;
78+
Extension p_ext;
6079
};
6180

6281
#endif

‎src/apps/builder/Builder.C‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ using std::vector;
178178
#include "Peptide.H"
179179
#include "PropertyIndexPanel.H"
180180
#include "PropertyPanel.H"
181+
#include "MoPanel.H"
181182
#include "PropertyPanelFactory.H"
182183
#include "ShapeDropDown.H"
183184
#include "StructLib.H"
@@ -5104,6 +5105,24 @@ void Builder::updatePropertyMenus()
51045105
for (int i = 0; i < 30; i++) { wxTheApp->Yield(true); wxMilliSleep(10); }
51055106
viewer->viewAll();
51065107
SceneScript run(viewer, sg, p_calculation, outdir);
5108+
// "mopanel <name>": press the MO panel's Compute, as a user would,
5109+
// recording every frame the Builder's own canvas paints meanwhile.
5110+
run.setExtension([this](SceneScript& s, const vector<string>& w) {
5111+
if (w[0] != "mopanel" || w.size() != 2)
5112+
return s.fail("unknown command: " + w[0]);
5113+
MoPanel *mo = 0;
5114+
set<PropertyPanel*> panels =
5115+
PropertyPanel::getPanels(p_calculation->getURL().toString());
5116+
for (set<PropertyPanel*>::iterator it = panels.begin();
5117+
it != panels.end() && !mo; ++it)
5118+
mo = dynamic_cast<MoPanel*>(*it);
5119+
if (!mo) return s.fail("mopanel: no MO panel");
5120+
return s.recordFrames(p_viewer, w[1], [mo]() {
5121+
wxCommandEvent ev(wxEVT_BUTTON, MoGUI::ID_BUTTON_MO_COMPUTE);
5122+
ev.SetEventObject(mo->FindWindow(MoGUI::ID_BUTTON_MO_COMPUTE));
5123+
mo->GetEventHandler()->ProcessEvent(ev);
5124+
});
5125+
});
51075126
if (!ready || !run.run(script)) {
51085127
string msg = ready ? run.message() : "molecule never loaded";
51095128
fprintf(stderr, "ECCE_VIEWER_SCENE: %s\n", msg.c_str());

‎src/inv/moiv/ChemIso.C‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555

5656
#ident "$Revision: 22147 $"
5757

58+
#include <cstdio>
59+
#include <cstdlib>
5860
#include "inv/actions/SoCallbackAction.H"
5961
#include "inv/actions/SoGetBoundingBoxAction.H"
6062
#include "inv/actions/SoGetMatrixAction.H"
@@ -485,4 +487,10 @@ RESET_TIMER();
485487

486488
isoLibIso(threshold.getValue());
487489
MARK_TIME("DONE with ISO");
490+
// ECCE_DEBUG_ISO: every surface actually generated, with its cutoff.
491+
static const bool isoDebug = getenv("ECCE_DEBUG_ISO") != 0;
492+
if (isoDebug) {
493+
fprintf(stderr, "ISO: ChemIso %p generated at threshold %g, %d vertices\n",
494+
(void *)this, threshold.getValue(), vp->vertex.getNum());
495+
}
488496
}

‎src/wxviz/viewer/SceneScript.C‎

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@
2121
#include "inv/misc/SoChildList.H"
2222
#include "inv/nodes/SoSeparator.H"
2323
#include "inv/ChemKit/ChemIso.H"
24+
#include "inv/ChemKit/ChemDetail.H"
25+
#include "inv/actions/SoCallbackAction.H"
26+
#include "inv/actions/SoRayPickAction.H"
27+
#include "inv/SoPickedPoint.H"
28+
#include "inv/SoPrimitiveVertex.H"
29+
#include "inv/nodes/SoShape.H"
30+
#include "viz/VRVector.H"
2431
#include "inv/misc/SoChildList.H"
2532

2633
#include "dsm/ICalculation.H"
@@ -524,6 +531,10 @@ bool SceneScript::exec(const vector<string>& w, const string& rest)
524531
return snapshot(w[1], 480, 0.2f, 0.3f, 0.4f);
525532
} else if (c == "thumb" && w.size() == 2) {
526533
return snapshot(w[1], 64, 0.0f, 0.0f, 0.0f);
534+
} else if (c == "nmcheck" && w.size() == 2) {
535+
return nmCheck(w[1]);
536+
} else if (p_ext) {
537+
return p_ext(*this, w);
527538
} else {
528539
return fail("unknown command: " + c);
529540
}
@@ -867,3 +878,137 @@ bool SceneScript::snapshot(const string& name, int size, float r, float g,
867878
writePpm(p_outdir + "/" + name + ".ppm", size, size, rend->getBuffer());
868879
return true;
869880
}
881+
882+
883+
namespace {
884+
885+
struct TipData { SbVec3f centre; float maxDist; };
886+
887+
void arrowTriangle(void *d, SoCallbackAction *a, const SoPrimitiveVertex *v1,
888+
const SoPrimitiveVertex *v2, const SoPrimitiveVertex *v3)
889+
{
890+
TipData *t = (TipData *)d;
891+
const SoPrimitiveVertex *v[3] = {v1, v2, v3};
892+
for (int i = 0; i < 3; i++) {
893+
SbVec3f p;
894+
a->getModelMatrix().multVecMatrix(v[i]->getPoint(), p);
895+
float dist = (p - t->centre).length();
896+
if (dist > t->maxDist) t->maxDist = dist;
897+
}
898+
}
899+
900+
} // namespace
901+
902+
// Two measurements from the scene as drawn, neither from NModeVectCmd's
903+
// arithmetic: how far the arrow's geometry reaches from the atom centre,
904+
// and where a ray along the arrow meets that atom's sphere.
905+
bool SceneScript::nmCheck(const string& name)
906+
{
907+
SGFragment *frag = p_sg->getFragment();
908+
SoWxRenderArea *area = findRenderArea(p_viewer);
909+
if (!frag || !area) return fail("nmcheck: no fragment or render area");
910+
SbViewportRegion vpr(SbVec2s(480, 480));
911+
SoSearchAction sa;
912+
sa.setType(VRVector::getClassTypeId(), FALSE);
913+
sa.setInterest(SoSearchAction::ALL);
914+
sa.apply(p_sg->getNMVecRoot());
915+
FILE *o = fopen((p_outdir + "/" + name + ".txt").c_str(), "w");
916+
if (!o) return fail("nmcheck: cannot write");
917+
fprintf(o, "# atom elem |d|_A tip_A sphere_A tip_minus_sphere_A verdict\n");
918+
for (int k = 0; k < sa.getPaths().getLength(); k++) {
919+
VRVector *vec = (VRVector *)sa.getPaths()[k]->getTail();
920+
const double *pos = frag->atomRef(k)->coordinates(), *dir = vec->getDirection();
921+
SbVec3f c((float)pos[0], (float)pos[1], (float)pos[2]);
922+
SbVec3f u((float)dir[0], (float)dir[1], (float)dir[2]);
923+
float dlen = u.length();
924+
TipData td = {c, 0.0f};
925+
SoCallbackAction ca;
926+
ca.addTriangleCallback(SoShape::getClassTypeId(), arrowTriangle, &td);
927+
ca.apply(vec);
928+
float sphere = -1.0f;
929+
if (dlen > 0.0f) {
930+
u /= dlen;
931+
const float L = 6.0f;
932+
SoRayPickAction rp(vpr);
933+
rp.setRay(c + u * L, -u, 0.0f, L);
934+
rp.setPickAll(TRUE);
935+
rp.apply(p_viewer->getTopNode());
936+
const SoPickedPointList& pl = rp.getPickedPointList();
937+
for (int i = 0; i < pl.getLength(); i++) {
938+
const ChemDetail *cd = dynamic_cast<const ChemDetail *>(pl[i]->getDetail());
939+
if (!cd) continue;
940+
int32_t ai = -1, bi = -1;
941+
cd->getAtomBondIndex(ai, bi);
942+
if (ai != k || bi >= 0) continue;
943+
float r = (pl[i]->getPoint() - c).length();
944+
if (r > sphere) sphere = r;
945+
}
946+
}
947+
const char *verdict = sphere < 0.0f ? "no-sphere-hit"
948+
: (td.maxDist <= sphere ? "INSIDE" : "outside");
949+
fprintf(o, "%d %s %.4f %.4f %.4f %.4f %s\n", k + 1,
950+
frag->atomRef(k)->atomicSymbol().c_str(), dlen, td.maxDist, sphere,
951+
td.maxDist - sphere, verdict);
952+
}
953+
fclose(o);
954+
return true;
955+
}
956+
957+
namespace {
958+
959+
struct FrameLog {
960+
SGContainer *sg;
961+
string prefix;
962+
FILE *log;
963+
int n;
964+
wxStopWatch clock;
965+
};
966+
967+
void logFrame(void *d)
968+
{
969+
FrameLog *f = (FrameLog *)d;
970+
GLint vp[4];
971+
glGetIntegerv(GL_VIEWPORT, vp);
972+
vector<unsigned char> px((size_t)vp[2] * vp[3] * 3);
973+
glFinish();
974+
glPixelStorei(GL_PACK_ALIGNMENT, 1);
975+
glReadPixels(vp[0], vp[1], vp[2], vp[3], GL_RGB, GL_UNSIGNED_BYTE, &px[0]);
976+
char buf[32];
977+
snprintf(buf, sizeof buf, "-%02d.ppm", f->n);
978+
writePpm(f->prefix + buf, vp[2], vp[3], &px[0]);
979+
// The thresholds the drawn isosurfaces carry at this frame, read from
980+
// the scene graph, not from whoever set them.
981+
SoSearchAction sa;
982+
sa.setType(ChemIso::getClassTypeId(), FALSE);
983+
sa.setInterest(SoSearchAction::ALL);
984+
sa.apply(f->sg->getMORoot());
985+
fprintf(f->log, "frame %02d t=%ldms %dx%d isosurfaces %d thresholds", f->n,
986+
f->clock.Time(), vp[2], vp[3], sa.getPaths().getLength());
987+
for (int k = 0; k < sa.getPaths().getLength(); k++)
988+
fprintf(f->log, " %.6g", ((ChemIso *)sa.getPaths()[k]->getTail())->threshold.getValue());
989+
fprintf(f->log, "\n");
990+
fflush(f->log);
991+
f->n++;
992+
}
993+
994+
} // namespace
995+
996+
bool SceneScript::recordFrames(wxWindow *canvasOwner, const string& name,
997+
std::function<void()> act)
998+
{
999+
SoWxRenderArea *area = findRenderArea(canvasOwner);
1000+
if (!area) return fail("recordframes: no render area");
1001+
FrameLog f;
1002+
f.sg = p_sg;
1003+
f.prefix = p_outdir + "/" + name;
1004+
f.log = fopen((f.prefix + "-frames.txt").c_str(), "w");
1005+
if (!f.log) return fail("recordframes: cannot write");
1006+
f.n = 0;
1007+
area->setFrameCallback(logFrame, &f);
1008+
act();
1009+
// Let any deferred redraw land too (1 s of event loop).
1010+
for (int i = 0; i < 100; i++) { wxTheApp->Yield(true); wxMilliSleep(10); }
1011+
area->setFrameCallback(0, 0);
1012+
fclose(f.log);
1013+
return true;
1014+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# #226: press the MO panel's Compute (default selection, the HOMO) and record
2+
# every frame the Builder's canvas paints, with the isosurface thresholds in
3+
# the scene at that frame (mo-compute-frames.txt).
4+
mopanel mo-compute
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# #228: normal-mode arrows against their atoms' spheres, calc-water-vib.
2+
# Modes are 0-based: 0 bend 1645, 1 sym. stretch 3866, 2 asym. stretch 3965.
3+
# nmcheck writes, per atom, the arrow tip's distance from the atom centre and
4+
# where a ray along the arrow meets the atom's sphere.
5+
viewall
6+
nmvect 1
7+
nmcheck nm1-default
8+
snap nm1-default
9+
nmvect 2
10+
nmcheck nm2-default
11+
snap nm2-default
12+
style Ball And Stick
13+
nmvect 1
14+
nmcheck nm1-ballstick
15+
snap nm1-ballstick
16+
nmvect 2
17+
nmcheck nm2-ballstick
18+
snap nm2-ballstick

0 commit comments

Comments
 (0)