vtir-wizard 1.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,34 @@
1
+ """vtir_wizard -- variable-temperature IR orchestrator for a Thermo Nicolet iS5
2
+ + Specac heated Golden Gate ATR + Specac USB temperature controller.
3
+
4
+ The package exposes three entry points:
5
+
6
+ vtir_wizard.orchestrator the run wizard (``vtir-wizard`` console script)
7
+ vtir_wizard.temp_plot live temperature/setpoint overlay window
8
+ vtir_wizard.ir_plot live stacked-IR-spectrum window
9
+
10
+ Only light-weight, dependency-free shared constants live in this top-level
11
+ module so the plot subprocesses (which import ``SCHEDULE_KINDS``) don't drag in
12
+ the orchestrator's pywin32 dependency.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ __version__ = "1.4.1"
17
+
18
+ # Single source of truth for per-direction metadata used across the wizard
19
+ # summary (glyph), the per-step log line (arrow), and the live plots' overlay
20
+ # shading (color, label). Adding a new schedule kind means adding one row here
21
+ # -- not editing four sites. Background ("bg") is also listed even though it
22
+ # isn't a step direction, because the live plots shade BG scans too.
23
+ SCHEDULE_KINDS = {
24
+ "up": {"glyph": "^", "arrow": "Heating up to",
25
+ "color": (1.0, 0.2, 0.2, 0.22), "label": "Sample (up)"},
26
+ "down": {"glyph": "v", "arrow": "Cooling down to",
27
+ "color": (1.0, 0.6, 0.0, 0.25), "label": "Sample (down)"},
28
+ "return": {"glyph": "*", "arrow": "Cooling back to starting temperature",
29
+ "color": (0.2, 0.7, 0.2, 0.28), "label": "Sample (return)"},
30
+ "bg": {"glyph": "B", "arrow": "(background, not a step)",
31
+ "color": (0.2, 0.2, 1.0, 0.18), "label": "Background"},
32
+ }
33
+
34
+ __all__ = ["__version__", "SCHEDULE_KINDS"]
vtir_wizard/config.py ADDED
@@ -0,0 +1,115 @@
1
+ """Config-file discovery and loading for the pip-installed wizard.
2
+
3
+ When the project lived as loose scripts the config sat next to ``vt_ir.py``.
4
+ Installed via pip the package lives in a read-only ``site-packages`` directory,
5
+ so the editable ``vt_ir_config.ini`` must live somewhere the user owns.
6
+
7
+ Resolution order (``resolve_config_path``):
8
+
9
+ 1. an explicit ``--config PATH`` (always wins)
10
+ 2. ``./vt_ir_config.ini`` in the current folder (per-experiment configs)
11
+ 3. ``%APPDATA%/vtir-wizard/vt_ir_config.ini`` (the per-user copy)
12
+
13
+ ``vtir-wizard --init-config`` writes the packaged template to the per-user
14
+ location so a fresh install has something to edit.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import configparser
19
+ import os
20
+ from pathlib import Path
21
+ from typing import Optional
22
+
23
+ APP_DIR_NAME = "vtir-wizard"
24
+ CONFIG_NAME = "vt_ir_config.ini"
25
+
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # Locations
29
+ # ---------------------------------------------------------------------------
30
+ def user_config_dir() -> Path:
31
+ """Per-user config directory. ``%APPDATA%/vtir-wizard`` on Windows (the
32
+ documented target); falls back to ``~/.config/vtir-wizard`` elsewhere so the
33
+ package still imports/tests on non-Windows CI."""
34
+ appdata = os.environ.get("APPDATA")
35
+ if appdata:
36
+ return Path(appdata) / APP_DIR_NAME
37
+ return Path.home() / ".config" / APP_DIR_NAME
38
+
39
+
40
+ def user_config_path() -> Path:
41
+ return user_config_dir() / CONFIG_NAME
42
+
43
+
44
+ def packaged_template_text() -> str:
45
+ """The bundled template ``vt_ir_config.ini`` shipped under the package's
46
+ ``data`` directory. Read through importlib.resources so it works from a
47
+ wheel, an editable install, or a source checkout."""
48
+ from importlib import resources
49
+
50
+ return (resources.files("vtir_wizard.data")
51
+ .joinpath(CONFIG_NAME)
52
+ .read_text(encoding="utf-8"))
53
+
54
+
55
+ # ---------------------------------------------------------------------------
56
+ # Resolution + loading
57
+ # ---------------------------------------------------------------------------
58
+ def resolve_config_path(explicit: Optional[str]) -> Optional[Path]:
59
+ """Return the config file to use, or ``None`` if nothing was found.
60
+
61
+ An explicit path that does not exist is a hard error (the user clearly meant
62
+ that file); the implicit search just falls through to the next candidate."""
63
+ if explicit:
64
+ p = Path(explicit).expanduser()
65
+ if not p.exists():
66
+ raise SystemExit(f"--config path does not exist: {p}")
67
+ return p
68
+ cwd = Path.cwd() / CONFIG_NAME
69
+ if cwd.exists():
70
+ return cwd
71
+ user = user_config_path()
72
+ if user.exists():
73
+ return user
74
+ return None
75
+
76
+
77
+ def load_config(path: Path) -> configparser.ConfigParser:
78
+ cfg = configparser.ConfigParser(inline_comment_prefixes=("#", ";"))
79
+ cfg.read(path, encoding="utf-8")
80
+ return cfg
81
+
82
+
83
+ def load_config_optional(path: Optional[Path]) -> configparser.ConfigParser:
84
+ """Tolerant load used by the plot helpers: returns an empty parser if the
85
+ path is missing, so a standalone plot window still starts."""
86
+ cfg = configparser.ConfigParser(inline_comment_prefixes=("#", ";"))
87
+ if path and Path(path).exists():
88
+ cfg.read(path, encoding="utf-8")
89
+ return cfg
90
+
91
+
92
+ def init_user_config(force: bool = False) -> tuple[Path, bool]:
93
+ """Write the packaged template to the per-user config location.
94
+
95
+ Returns ``(path, written)`` -- ``written`` is False if the file already
96
+ existed and ``force`` was not given (so we never clobber a tuned config by
97
+ accident)."""
98
+ dest = user_config_path()
99
+ dest.parent.mkdir(parents=True, exist_ok=True)
100
+ if dest.exists() and not force:
101
+ return dest, False
102
+ dest.write_text(packaged_template_text(), encoding="utf-8")
103
+ return dest, True
104
+
105
+
106
+ def missing_config_message() -> str:
107
+ """Actionable message when no config is found anywhere."""
108
+ return (
109
+ "No vt_ir_config.ini found.\n"
110
+ f"Looked for: ./{CONFIG_NAME} (current folder) and {user_config_path()}.\n"
111
+ "Create the per-user copy with:\n"
112
+ " vtir-wizard --init-config\n"
113
+ "then edit it (the path is printed) and re-run. Or pass an explicit\n"
114
+ "file with --config <path>."
115
+ )
@@ -0,0 +1,2 @@
1
+ # Marks vtir_wizard.data as a subpackage so the bundled vt_ir_config.ini template
2
+ # is reliably importable via importlib.resources.files("vtir_wizard.data").
@@ -0,0 +1,231 @@
1
+ # =============================================================================
2
+ # vt_ir_config.ini -- machine-specific paths and defaults for the VT-IR wizard
3
+ # -----------------------------------------------------------------------------
4
+ # Edit the values below to match the lab PC. Most lines have a comment that
5
+ # explains what changes if you tweak the value. All path values are plain
6
+ # Windows paths (backslashes are fine -- no escaping needed in INI files).
7
+ # =============================================================================
8
+
9
+ [paths]
10
+ # ---------------------------------------------------------------------------
11
+ # Specac USB controller CLI. Installed by the Specac controller installer at
12
+ # this default location. Confirm with: where specac.cmd.exe
13
+ # ---------------------------------------------------------------------------
14
+ specac_exe = C:\Program Files (x86)\Specac\Temperature Controller\1.0\bin\specac.cmd.exe
15
+
16
+ # ---------------------------------------------------------------------------
17
+ # Where all collected spectra are written. One sub-folder is created per
18
+ # experiment, named after the sample (sanitized).
19
+ # ---------------------------------------------------------------------------
20
+ output_root = C:\Users\<you>\Documents\VT-IR\spectra
21
+
22
+ # ---------------------------------------------------------------------------
23
+ # Where to look for background files in sample mode. Usually the same as
24
+ # output_root -- BG files live in <bg_root>/<sample>/BG_<sample>_<T>C.SPA.
25
+ # Override only if you collect BG files in a separate location.
26
+ # ---------------------------------------------------------------------------
27
+ bg_root = C:\Users\<you>\Documents\VT-IR\spectra
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Where the wizard drops its timestamped session log files.
31
+ # ---------------------------------------------------------------------------
32
+ log_dir = C:\Users\<you>\Documents\VT-IR\logs
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # Where the Specac controller GUI writes its temperature .log files.
36
+ # Used by the temperature overlay plot to find the active log.
37
+ # ---------------------------------------------------------------------------
38
+ specac_log_dir = C:\Users\<you>\Documents\Specac Temperature Controller\Logs
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # Where the live overlay plot saves its SVG snapshots. Parallel to the
42
+ # spectra and log folders. One sub-folder per sample is created here, and
43
+ # snapshots are named <sample>_<BG|SAMPLE>_<timestamp>.svg so background and
44
+ # sample plots stay distinguishable. If left blank, defaults to a "plots"
45
+ # folder next to output_root.
46
+ # ---------------------------------------------------------------------------
47
+ plot_dir = C:\Users\<you>\Documents\VT-IR\plots
48
+
49
+
50
+ [defaults]
51
+ # ---------------------------------------------------------------------------
52
+ # Default OMNIC experiment file (.exp). Loaded once at the start of each run
53
+ # with the KeepBkg flag so subsequent BackgroundFileName pokes survive.
54
+ # ---------------------------------------------------------------------------
55
+ omnic_exp_file = C:\my documents\omnic\Param\VT_128_2.exp
56
+
57
+ # Default temperature program -- accepts the (Tmin,Tmax,Tstep) range syntax or
58
+ # a whitespace-separated list "50 75 100 150".
59
+ temps_default = (50,200,20)
60
+
61
+ # Allowed temperature window in C. Hard guard against typos and against
62
+ # exceeding the ATR cell rating.
63
+ tmin = 20
64
+ tmax = 250
65
+
66
+ # Extra dwell at each set point after the Specac CLI returns "temperature
67
+ # reached". Useful for letting the sample (not just the cell body) reach
68
+ # thermal equilibrium, and for damping any residual gradient near tolerance.
69
+ # Default 120 s (2 min); tune down to ~30 s for fast empty-ATR test runs.
70
+ equilibration_s = 120
71
+
72
+
73
+ [omnic]
74
+ # ---------------------------------------------------------------------------
75
+ # How long to wait for a single CollectBackground / CollectSample to finish.
76
+ # pywin32's DDE Exec call has a hard 60-second internal timeout, which is far
77
+ # shorter than a real 128-scan @ 2 cm-1 collection -- so the wizard issues
78
+ # collect commands with the 'Polling' keyword and waits in Python by polling
79
+ # OMNIC's MenuStatus. The two knobs below govern that polling loop.
80
+ # ---------------------------------------------------------------------------
81
+ # How often to query MenuStatus while waiting.
82
+ poll_interval_s = 2
83
+
84
+ # Hard cap on a single collection. 1800 s = 30 minutes; bump up only if your
85
+ # experiment file legitimately runs longer than that per spectrum.
86
+ collect_timeout_s = 1800
87
+
88
+ # If OMNIC refuses to START a collection (the Polling Exec blocks ~60 s and
89
+ # raises a DDE error -- usually the iS5 bench has gone offline / OMNIC is
90
+ # wedged), the orchestrator reconnects the DDE channel and retries this many
91
+ # times before giving up with an actionable message. Set to 0 to disable
92
+ # retrying. Note: a hard bench wedge usually needs OMNIC restarted; retrying
93
+ # only recovers transient DDE-channel glitches.
94
+ collect_retries = 2
95
+
96
+ # Seconds to wait between those retries.
97
+ retry_delay_s = 5
98
+
99
+
100
+ [live_plot]
101
+ # ---------------------------------------------------------------------------
102
+ # Auto-launch the temperature/setpoint overlay window when a run starts. It
103
+ # opens in its own console window AFTER the "Ready?" confirmation, locked onto
104
+ # the current sample's session folder. Close the window to dismiss it; the
105
+ # orchestrator continues regardless.
106
+ # ---------------------------------------------------------------------------
107
+ # Set to false to disable auto-launch (or pass --no-live-plot on the CLI; note
108
+ # --no-live-plot disables BOTH the temperature overlay and the IR stack window).
109
+ enabled = true
110
+
111
+ # Refresh interval in seconds.
112
+ interval_s = 5
113
+
114
+ # Estimated scan duration in seconds (used to draw the shaded scan band).
115
+ # 210 s ~= 128 scans @ 2 cm-1. Match this to your OMNIC .exp file.
116
+ scan_duration_s = 210
117
+
118
+ # How long after a run finishes the live plot waits before auto-saving its
119
+ # SVG snapshot. The delay lets the passive cool-down tail show up in the
120
+ # saved image. 600 s = 10 min. The plot ALSO saves immediately if you close
121
+ # its window manually.
122
+ save_delay_s = 600
123
+
124
+
125
+ [ir_plot]
126
+ # ---------------------------------------------------------------------------
127
+ # Auto-launch the live stacked-IR-spectrum window alongside the temperature
128
+ # overlay. It re-reads the session's .SPA files as they land and re-draws a
129
+ # temperature-colored waterfall of every scan so far. After each new scan it
130
+ # overwrites a SINGLE SVG at <plot_dir>/<sample>/<sample>_IR_stack.svg (only
131
+ # the final stacked spectrum persists), in the same plot_dir as the temperature
132
+ # snapshots.
133
+ # ---------------------------------------------------------------------------
134
+ # Set to false to disable just this window (or pass --no-ir-plot on the CLI).
135
+ enabled = true
136
+
137
+ # Layout: stack = fixed-offset waterfall (default)
138
+ # overlay = all spectra on a shared baseline
139
+ mode = stack
140
+
141
+ # Display unit: A = absorbance, T = %transmittance.
142
+ unit = A
143
+
144
+ # Refresh interval in seconds.
145
+ interval_s = 5
146
+
147
+ # Major x-tick spacing in cm-1 (0 = let matplotlib choose).
148
+ tick_step = 500
149
+
150
+ # Fixed vertical offset between stacked spectra. Leave blank for an automatic
151
+ # offset scaled to the data; set a number to pin it (stack mode only).
152
+ offset =
153
+
154
+ # Matplotlib colormap used to color spectra by temperature.
155
+ cmap = gnuplot2
156
+
157
+ # Figure sizing. The saved SVG's HEIGHT grows with the number of spectra so a
158
+ # tall stack keeps its peaks legible instead of squashing them into a fixed
159
+ # canvas. Raise height_per_spectrum to give each spectrum more vertical room.
160
+ # fig_width - figure width in inches
161
+ # height_per_spectrum - inches of height added per stacked scan (stack mode)
162
+ # max_height - cap on the saved figure height in inches
163
+ fig_width = 10
164
+ height_per_spectrum = 0.5
165
+ max_height = 30
166
+
167
+
168
+ [export]
169
+ # ---------------------------------------------------------------------------
170
+ # Every collected spectrum is always saved as OMNIC's native .SPA (needed if
171
+ # you ever want to re-process in OMNIC). In addition, each spectrum is saved
172
+ # in the formats listed below -- OMNIC picks the format from the extension.
173
+ #
174
+ # Comma-separated list of extensions (without the dot). Common choices:
175
+ # csv -> comma/tab-separated X,Y text (best for most plotting tools)
176
+ # jdx -> JCAMP-DX spectroscopy text (.dx is the same format; use 'jdx'
177
+ # if your reader is picky -- it's the
178
+ # extension OMNIC documents)
179
+ # tif -> image
180
+ # Leave blank to save ONLY .SPA. Examples: csv | csv,jdx | (blank)
181
+ # ---------------------------------------------------------------------------
182
+ additional_formats = csv
183
+
184
+
185
+ [backgrounds]
186
+ # ---------------------------------------------------------------------------
187
+ # How sample collections pick the background to ratio against. Backgrounds
188
+ # do NOT have to match the sample temperature exactly -- the heated cell sits
189
+ # in a controlled glovebox -- so mismatches and old backgrounds are WARNED
190
+ # about (and logged for the audit trail) but never block a run.
191
+ # ---------------------------------------------------------------------------
192
+ # Default temperature-matching mode (the wizard asks each run, pre-filled with
193
+ # this):
194
+ # exact - use the BG collected at each sample temperature (fails only if a
195
+ # temperature has no BG at all -- then it suggests closest/fixed)
196
+ # closest - use the nearest-temperature BG available for each step
197
+ # fixed - use ONE background (chosen at the prompt) for every step
198
+ match_mode = exact
199
+
200
+ # Warn (but never block) when a chosen background file is older than this many
201
+ # days. The warning is written to the run log so there is a record of exactly
202
+ # which (possibly old) background each measurement used.
203
+ max_age_warn_days = 1
204
+
205
+ # Minutes written to OMNIC's Collect/MaxBackgroundAge (with BackgroundHandling =
206
+ # AfterTime) so OMNIC silently reuses the background the wizard binds instead of
207
+ # stopping to ask "the background is old -- collect a new one?". That prompt is
208
+ # a modal dialog that blocks the DDE call and hangs the run, so this is set huge
209
+ # by default (~1.9 years). The wizard applies this over DDE every run, but the
210
+ # primary fix is to ALSO select "Background after N minutes" with this value in
211
+ # the OMNIC experiment (.exp) and SAVE it. Lower only if you actually want OMNIC
212
+ # to force a fresh background after some age.
213
+ max_bg_age_min = 999999
214
+
215
+
216
+ [heater]
217
+ # ---------------------------------------------------------------------------
218
+ # Specac controller settings. Applied at the start of every run.
219
+ # ---------------------------------------------------------------------------
220
+ # +/- tolerance window (in deg C) used by the "sp <T> w" wait.
221
+ tolerance_c = 1
222
+
223
+ # Ramp rate in C/min. Specac default 50 has been calibrated; only change with
224
+ # a re-calibration (the old make_temperature_program.py picked timings based on
225
+ # this value).
226
+ ramp_c_per_min = 50
227
+
228
+ # Timeout for any single "go to setpoint and wait" call, in seconds. 3600 = 1h.
229
+ # Generous -- a real ramp 30 -> 250 C at 50 C/min plus settling is well under
230
+ # 10 minutes, so this only fires if the controller hangs.
231
+ ramp_timeout_s = 3600