Skip to content

Commit f4e7269

Browse files
andyclaude
andcommitted
Merge wip/214-spectrum: vibrational spectrum viewer (wavenumber high to low, IR and Raman panes, band zoom, Open in window, compact layout) replaces wxPlotCtrl in the Vibrations panel (#214)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y1pWhDKvhakdnjg3taYGxM
2 parents 554417f + 96c9bed commit f4e7269

18 files changed

Lines changed: 3073 additions & 84 deletions

File tree

‎CMakeLists.txt‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,12 @@ ecce_app(render-rgb tests/look/imageconv WX GL
493493
LINK eccewxviz eccewxinv eccevizsg eccemoiv ecceinv eccewxgui eccewxplotctrl eccewxthings eccedsi eccedav eccecipc eccefaces eccexml eccetdat ecceutil ${ECCE_X11_LIBS} jpeg freetype)
494494
set_target_properties(render-rgb PROPERTIES EXCLUDE_FROM_ALL TRUE)
495495
496+
# Paints the spectrum canvas onto a bitmap (tests/spectrum, #214); `ninja
497+
# spectrum-render`, needs a display (xvfb-run).
498+
ecce_app(spectrum-render tools/spectrum WX LINK eccetdat ecceutil)
499+
set_target_properties(spectrum-render PROPERTIES EXCLUDE_FROM_ALL TRUE)
500+
target_include_directories(spectrum-render PRIVATE ${CMAKE_SOURCE_DIR}/src/apps/builder)
501+
496502
# Coin only: the EGL offscreen path with no wx and no display.
497503
if(ECCE_USE_COIN)
498504
add_executable(offscreen-check EXCLUDE_FROM_ALL tools/coin/offscreen/offscreen-check.C)
@@ -1610,6 +1616,23 @@ target_link_libraries(throwlog_test PRIVATE ecceutil)
16101616
add_test(NAME throwlog COMMAND throwlog_test)
16111617
add_test(NAME throwlog-all COMMAND throwlog_test all)
16121618

1619+
# tests/spectrum -- the wx-free IR/Raman spectrum model (#214): ticks, axis
1620+
# reversal and zoom, broadening, sticks.
1621+
add_executable(vibspectrum_test tests/spectrum/vibspectrum_test.C)
1622+
target_link_libraries(vibspectrum_test PRIVATE eccetdat ecceutil)
1623+
add_test(NAME vibspectrum COMMAND vibspectrum_test)
1624+
# The canvas painted onto a bitmap (tools/spectrum/render) from the numbers
1625+
# the codes printed; needs wx, g++ and xvfb-run, SKIPs without them.
1626+
find_program(PYTHON3_FOR_SPECTRUM NAMES python3)
1627+
if(PYTHON3_FOR_SPECTRUM)
1628+
add_test(NAME spectrum_canvas
1629+
COMMAND ${PYTHON3_FOR_SPECTRUM}
1630+
${CMAKE_CURRENT_SOURCE_DIR}/tests/spectrum/run_tests.py
1631+
${CMAKE_BINARY_DIR}/spectrum-render)
1632+
set_tests_properties(spectrum_canvas PROPERTIES TIMEOUT 600
1633+
SKIP_RETURN_CODE 77)
1634+
endif()
1635+
16131636
# tests/session -- the session id and key (#233): shell and C++ derivations
16141637
# agree, and both generators make well-formed, distinct ids.
16151638
add_executable(session_key_test tests/session/session_key_test.C)

‎docs/claude/index.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ holds its overview and lists its entries.
1212
- [Code registration](codereg/index.md) — 50 entries
1313
- [Services](services/index.md) — 4 entries
1414
- [MO correlation diagram](mo-diagram/index.md) — 8 entries
15-
- [wx, viewer and C++](wx-viewer/index.md) — 12 entries
15+
- [wx, viewer and C++](wx-viewer/index.md) — 13 entries

‎docs/claude/wx-viewer/index.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,4 @@ Read this before changing any dialog, panel, sizer, grid, ewx control or the Ope
3333
- [RESOLVED (#81, fixed `9a3004e`, confirmed live 2026-09-21): the Vibrational Frequencies panel's Animation/Vector radio box did not deliver its click event under wx3.2/GTK3](resolved-81-fixed-9a3004e-confirmed-live-2026.md)
3434
- [ChemDisplay's `glPopAttrib` undoes what Coin's lazy element sent inside it; the first offscreen render drew an ESP surface unlit](chemdisplay-glpopattrib-undoes-lazy-element-sends.md)
3535
- [The unit cell is drawn only while the Periodic Builder panel is open](unit-cell-drawn-only-by-the-periodic-builder.md)
36+
- [The Vibrational Frequencies graph is `SpectrumCanvas` over the wx-free `VibSpectrum` model (#214)](vibrational-spectrum-canvas-214.md)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
type: map
3+
title: "The Vibrational Frequencies graph is `SpectrumCanvas` over the wx-free `VibSpectrum` model (#214); look at `tools/spectrum/render` output, and mind the traps below"
4+
area: wx-viewer
5+
section: ""
6+
paths: ["include/tdat/VibSpectrum.H", "src/tdat/chemistry/VibSpectrum.C", "src/apps/builder/SpectrumCanvas.H", "src/apps/builder/NModePanel.C", "tools/spectrum", "tests/spectrum"]
7+
issues: [214, 173]
8+
---
9+
`NModePanel` no longer uses `ewxPlotCtrl`. `VibSpectrum` (sticks, scale,
10+
broadening, `SpectrumAxis`, ticks) is wx-free and tested by
11+
`vibspectrum_test`; `SpectrumCanvas.H` only paints it and is the same
12+
code `tools/spectrum/render` paints onto a bitmap (`xvfb-run`).
13+
`tests/spectrum/run_tests.py` compares the canvas with the numbers read
14+
from the raw fixtures by `oracle.py` (no ECCE parser involved);
15+
`tests/spectrum/capture.py` does the same through the real Builder.
16+
17+
- **Intensities in the units the code printed.** The pane axis is
18+
km/mol or Å⁴/amu only for the unit strings `knownIntensityUnits()`
19+
recognises (`KM/Mole`, `A^4/AMU`, `A**4/AMU`); anything else is drawn
20+
relative (largest stick 1). A new code with another spelling needs it
21+
added there.
22+
- **All-zero intensities mean "not computed".** The Gaussian fixture
23+
stores `VIBRAM` as zeros without `Raman=`; such a property gets no pane.
24+
- **Near-zero modes are not drawn.** ORCA lists the 6 translations and
25+
rotations as 0.00 and NWChem as about 1e-5 among the modes, so mode
26+
numbers do not start at 1 for the first stick. `zeroTolerance()` is 10
27+
cm-1; a real imaginary mode below that magnitude is lost.
28+
- **Imaginary modes are drawn at their negative wavenumber** in the
29+
warning colour, listed in the hover, and left out of the envelope.
30+
- **The envelope is area-normalised**, so its y values are
31+
intensity per cm-1 and sit on a second, right-hand axis next to the
32+
stick axis; they are not comparable with the stick heights.
33+
- **Hooks:** `ECCE_SPECTRUM_DUMP=<path>` (with `ECCE_SPECTRUM_CLICK=<mode>`
34+
and `ECCE_EXIT_AFTER_DUMP=1`) writes what the panel's canvas holds and
35+
paints it to `<path>.png`.
36+
- `tools/spectrum/render` is not in CMake; the test compiles it with
37+
`wx-config`, like the MO diagram's canvas check.

‎include/tdat/VibSpectrum.H‎

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/**
2+
* @file
3+
*
4+
* A vibrational (IR / Raman) spectrum as the Vibrational Frequencies
5+
* panel shows it (#214).
6+
*
7+
* No wx and no drawing: which sticks exist, what a scaling factor and a
8+
* line width do to them, which wavenumber range is on screen and where
9+
* the ticks go are decided here, so they can be tested from the command
10+
* line. SpectrumCanvas.H only paints what this holds.
11+
*/
12+
13+
#ifndef VIBSPECTRUM_HH
14+
#define VIBSPECTRUM_HH
15+
16+
#include <string>
17+
#include <vector>
18+
using std::string;
19+
using std::vector;
20+
21+
/** The two spectra a vibrational calculation can give. */
22+
enum VibKind { VIB_IR = 0, VIB_RAMAN = 1 };
23+
24+
/** The wavenumber axis: a window on the data and its direction. */
25+
class SpectrumAxis
26+
{
27+
public:
28+
SpectrumAxis();
29+
30+
/** Set the whole range the data spans; resets the view to it. */
31+
void setFull(double lo, double hi);
32+
double fullLo() const { return p_fullLo; }
33+
double fullHi() const { return p_fullHi; }
34+
35+
double lo() const { return p_lo; }
36+
double hi() const { return p_hi; }
37+
double span() const { return p_hi - p_lo; }
38+
bool zoomed() const;
39+
40+
/** High wavenumbers on the left, the convention for IR and Raman. */
41+
void setReversed(bool reversed) { p_reversed = reversed; }
42+
bool reversed() const { return p_reversed; }
43+
44+
/** 0 at the left edge of the plot, 1 at the right. */
45+
double toFraction(double wavenumber) const;
46+
double fromFraction(double fraction) const;
47+
48+
/** Show [a, b] (either order), clipped to the full range. */
49+
void zoomTo(double a, double b);
50+
/** Scale the window by factor (<1 zooms in) about a wavenumber. */
51+
void zoomAbout(double centre, double factor);
52+
/** Move the window by a wavenumber difference, staying in range. */
53+
void pan(double delta);
54+
void reset();
55+
56+
/** Narrowest window allowed, cm-1. */
57+
static double minSpan() { return 20.0; }
58+
59+
private:
60+
void clamp();
61+
62+
double p_fullLo, p_fullHi, p_lo, p_hi;
63+
bool p_reversed;
64+
};
65+
66+
/** Tick positions for an axis: round numbers, 1, 2 or 5 times 10^n. */
67+
struct SpectrumTicks
68+
{
69+
vector<double> values;
70+
double step;
71+
int decimals; // digits after the point a label needs
72+
};
73+
74+
/** Ticks inside [lo, hi], at most about `target` of them. */
75+
SpectrumTicks niceTicks(double lo, double hi, int target);
76+
77+
/**
78+
* A y axis from 0 up: `top` is the first tick at or above the maximum,
79+
* so the tallest feature is never clipped.
80+
*/
81+
struct SpectrumYAxis
82+
{
83+
double top;
84+
SpectrumTicks ticks;
85+
};
86+
SpectrumYAxis niceYAxis(double maximum, int target);
87+
88+
/** One mode, with the quantities drawn for it. */
89+
struct VibStick
90+
{
91+
int mode; // zero-based row of the frequency table
92+
double wavenumber; // scaled, cm-1; negative if imaginary
93+
double intensity; // as drawn (see VibSpectrum::unitLabel)
94+
string irrep;
95+
bool imaginary;
96+
};
97+
98+
enum LineShape { LINE_GAUSSIAN, LINE_LORENTZIAN };
99+
100+
class VibSpectrum
101+
{
102+
public:
103+
VibSpectrum();
104+
105+
/**
106+
* The modes, from VIBFREQ and (optionally) VIBSYM. Clears any
107+
* intensities. `irreps` may be empty or shorter than the list.
108+
*/
109+
void setModes(const vector<double>& wavenumbers,
110+
const vector<string>& irreps);
111+
112+
/**
113+
* Intensities for every mode, in the code's own `units` as written
114+
* in the property (e.g. "KM/Mole", "A^4/AMU"). A units string that
115+
* is not recognised makes the spectrum relative: drawn scaled so
116+
* its largest stick is 1.
117+
*/
118+
void setIntensities(VibKind kind, const vector<double>& values,
119+
const string& units);
120+
121+
bool has(VibKind kind) const { return p_has[kind]; }
122+
int modeCount() const { return (int)p_freq.size(); }
123+
124+
/** Axis title for the pane, e.g. "IR intensity (km/mol)". */
125+
string axisLabel(VibKind kind) const;
126+
/** Just the unit, UTF-8: "km/mol", "Å⁴/amu", "relative". */
127+
string unitLabel(VibKind kind) const;
128+
bool relative(VibKind kind) const { return p_relative[kind]; }
129+
130+
/** Frequency scaling factor, applied to every wavenumber. */
131+
void setScale(double scale) { p_scale = scale > 0 ? scale : 1.0; }
132+
double scale() const { return p_scale; }
133+
134+
/**
135+
* Full width at half maximum of the broadening, cm-1. 0 means
136+
* sticks only.
137+
*/
138+
void setFwhm(double fwhm) { p_fwhm = fwhm > 0 ? fwhm : 0.0; }
139+
double fwhm() const { return p_fwhm; }
140+
void setLineShape(LineShape shape) { p_shape = shape; }
141+
LineShape lineShape() const { return p_shape; }
142+
143+
/**
144+
* Wavenumbers below this magnitude are the translations and
145+
* rotations some codes list among the modes (ORCA prints 0.00, NWChem
146+
* values of 1e-5). They are not vibrations, so they get no stick.
147+
*/
148+
static double zeroTolerance() { return 10.0; }
149+
150+
/** Scaled wavenumber of mode i. */
151+
double wavenumber(int i) const { return p_freq[i] * p_scale; }
152+
bool isVibration(int i) const;
153+
bool isImaginary(int i) const;
154+
155+
/**
156+
* The sticks of one spectrum: real vibrations and imaginary modes,
157+
* not the near-zero ones. A mode whose intensity is zero stays in
158+
* the list, so it can still be hovered and so the table row it
159+
* belongs to is known.
160+
*/
161+
vector<VibStick> sticks(VibKind kind) const;
162+
163+
/**
164+
* The broadened spectrum at a wavenumber, in intensity per cm-1:
165+
* each real mode contributes an area-normalised line, so the
166+
* integral of a band is its stick intensity whatever the width and
167+
* overlapping bands add. Imaginary modes contribute nothing.
168+
*/
169+
double envelope(VibKind kind, double wavenumber) const;
170+
171+
/** Largest stick height with wavenumber in [lo, hi]; 0 if none. */
172+
double maxStick(VibKind kind, double lo, double hi) const;
173+
/** Largest envelope value in [lo, hi] (0 when fwhm is 0). */
174+
double maxEnvelope(VibKind kind, double lo, double hi) const;
175+
176+
/**
177+
* The wavenumber range worth showing by default: the modes plus a
178+
* margin, on round numbers. Reaches below zero only when there is
179+
* an imaginary mode.
180+
*/
181+
void defaultRange(double *lo, double *hi) const;
182+
183+
/** Area-normalised line shape, 1/cm-1, centred on zero. */
184+
static double lineValue(LineShape shape, double offset, double fwhm);
185+
186+
/** Whether the code's units string is one we know how to label. */
187+
static bool knownIntensityUnits(VibKind kind, const string& units);
188+
189+
private:
190+
double drawn(VibKind kind, int i) const;
191+
192+
vector<double> p_freq;
193+
vector<string> p_irrep;
194+
vector<double> p_inten[2];
195+
bool p_has[2];
196+
bool p_relative[2];
197+
string p_units[2];
198+
double p_scale, p_fwhm;
199+
LineShape p_shape;
200+
};
201+
202+
#endif

‎src/apps/builder/Builder.C‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4349,6 +4349,7 @@ void Builder::loadPaneLayout(const wxString& layoutName_, const bool& update)
43494349
// above: SafeSet() just copied the saved min_size.x verbatim.
43504350
wxSize minSize = p.min_size;
43514351
minSize.x = contentMinWidth(p.window);
4352+
minSize.y = std::max(minSize.y, panel->minimumHeight());
43524353
p.MinSize(minSize);
43534354
// addPropertyPanel() ticked this panel's Property-menu item from
43544355
// the pane's visibility as it added it, which was BEFORE the line
@@ -5401,7 +5402,8 @@ void Builder::addPropertyPanel(PropertyPanel *panel, const string& name)
54015402
// the play button beside it visible. createPropertyPanel() already
54025403
// gives every property panel SetMinSize(400, -1); honour it.
54035404
int paneMinWidth = contentMinWidth(panel);
5404-
info.MinSize(wxSize(paneMinWidth, PANEL_HEIGHT_MIN));
5405+
info.MinSize(wxSize(paneMinWidth,
5406+
std::max(PANEL_HEIGHT_MIN, panel->minimumHeight())));
54055407
info.BestSize(wxSize(paneMinWidth, paneHeight));
54065408
info.dock_proportion = paneHeight;
54075409

0 commit comments

Comments
 (0)