irspec 0.1.0__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.
irspec/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ """irspec -- infrared spectra for chemistry students (see README.md and CHANGELOG.md)."""
2
+ __version__ = "0.1.0"
irspec/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ """python -m irspec == the irspec command."""
2
+ from .cli import main
3
+
4
+ main()
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env python3
2
+ """absorbance_to_transmittance.py -- convert an IR spectrum's y-axis between
3
+ Absorbance and %Transmittance for nulabreport figures.
4
+
5
+ WHY THIS EXISTS
6
+ nulabreport's \\irspectrum / \\irspectrumcompare macros just plot whatever
7
+ y-column a CSV gives them; they do not know or care whether that column
8
+ is Absorbance or %Transmittance. When a report puts several reference
9
+ spectra on comparable axes (this lab's own OMNIC exports are always
10
+ %Transmittance, 0-100), a source that is natively Absorbance has to be
11
+ converted on purpose -- silently mixing the two on one figure, or on
12
+ stacked panels a reader will visually compare, misleads regardless of
13
+ axis labels. This script does that conversion, once, in a place that
14
+ documents the formula, rather than as an unrepeatable one-off snippet
15
+ buried in a chat session.
16
+
17
+ Do NOT convert the other direction (Transmittance -> Absorbance) as a
18
+ way to "normalise everything to Absorbance" instead -- this lab's own
19
+ instrument output and house convention are %Transmittance; Absorbance
20
+ sources get converted to match, not the reverse. convert_series() below
21
+ supports both directions for completeness (and because the algebra is
22
+ symmetric), but the CLI defaults to --to transmittance for that reason.
23
+
24
+ THE FORMULA
25
+ Absorbance and (fractional) Transmittance are related by the
26
+ Beer-Lambert definition A = -log10(T), i.e. T = 10^(-A), where T runs
27
+ 0-1. This lab's %Transmittance convention is just T x 100:
28
+
29
+ %T = 100 x 10^(-A)
30
+ A = -log10(%T / 100)
31
+
32
+ Both directions are monotonic and well-defined for A >= 0 (equivalently
33
+ 0 < %T <= 100); A < 0 (%T > 100) can occur in noisy baseline data and is
34
+ passed through the same formula rather than clipped, so a slightly
35
+ negative-absorbance noise point becomes a slightly-over-100 %T point
36
+ instead of silently disappearing.
37
+
38
+ INPUT / OUTPUT FORMAT
39
+ Same "plain" CSV convention used everywhere else in this pipeline
40
+ (spa_tool.py's write_plain_csv(), jdx_tool.py's decoded output): a
41
+ one-line header "wavenumber,intensity" followed by comma-delimited
42
+ rows, decimal point (not comma), ascending or descending wavenumber
43
+ order preserved as given -- this script does not resort or resample,
44
+ it only rewrites the intensity column in place.
45
+
46
+ USAGE
47
+ python3 absorbance_to_transmittance.py IN.csv --out OUT.csv
48
+ python3 absorbance_to_transmittance.py IN.csv --out OUT.csv --to absorbance
49
+
50
+ IN.csv and OUT.csv may be the same path (the file is fully read before
51
+ anything is written).
52
+
53
+ EXAMPLE (this report's actual use, 2026-09-18)
54
+ literature/webbook/plain/PROPIOPHENONE.csv was decoded from the NIST
55
+ Chemistry WebBook's native Absorbance .jdx (see
56
+ literature/webbook/decode_notes.md) and left as Absorbance in an
57
+ earlier pass of this report so as not to silently misrepresent the
58
+ source. Once the report started stacking it directly against
59
+ Transmittance curves in \\irspectrumcompare figures, the honest fix was
60
+ to convert it, not just label it:
61
+
62
+ python3 nulabreport/tools/absorbance_to_transmittance.py \\
63
+ literature/webbook/plain/PROPIOPHENONE.csv \\
64
+ --out literature/webbook/plain/PROPIOPHENONE.csv
65
+
66
+ The original Absorbance values are not lost: they still live untouched
67
+ in the raw source, literature/webbook/csv/PROPIOPHENONE.csv (semicolon-
68
+ delimited, decimal-comma, as downloaded), which this script never
69
+ reads or writes -- only the derived "plain" CSV is rewritten.
70
+ """
71
+
72
+ import argparse
73
+ import csv
74
+ import math
75
+
76
+
77
+ def convert_series(intensities, to="transmittance"):
78
+ """Convert a list of y-values between Absorbance and %Transmittance.
79
+
80
+ to="transmittance": treats input as Absorbance, returns %Transmittance
81
+ via %T = 100 * 10**(-A).
82
+ to="absorbance": treats input as %Transmittance, returns Absorbance
83
+ via A = -log10(%T / 100).
84
+ """
85
+ if to == "transmittance":
86
+ return [100.0 * (10.0 ** (-a)) for a in intensities]
87
+ elif to == "absorbance":
88
+ out = []
89
+ for pct_t in intensities:
90
+ if pct_t <= 0:
91
+ raise ValueError(
92
+ f"%T value {pct_t!r} is <= 0; cannot take log10 for "
93
+ "Absorbance conversion"
94
+ )
95
+ out.append(-math.log10(pct_t / 100.0))
96
+ return out
97
+ else:
98
+ raise ValueError(f"unknown --to target: {to!r}")
99
+
100
+
101
+ def read_plain_csv(path):
102
+ with open(path, newline="") as f:
103
+ reader = csv.reader(f)
104
+ header = next(reader)
105
+ rows = [(row[0], float(row[1])) for row in reader if row]
106
+ return header, rows
107
+
108
+
109
+ def write_plain_csv(path, header, wavenumbers, intensities, digits=4):
110
+ with open(path, "w", newline="") as f:
111
+ writer = csv.writer(f)
112
+ writer.writerow(header)
113
+ for wn, inten in zip(wavenumbers, intensities):
114
+ writer.writerow([wn, f"{inten:.{digits}f}"])
115
+
116
+
117
+ def main():
118
+ ap = argparse.ArgumentParser(
119
+ description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
120
+ )
121
+ ap.add_argument("csv_in", help="input plain CSV (wavenumber,intensity)")
122
+ ap.add_argument("--out", required=True, help="output CSV path (may equal csv_in)")
123
+ ap.add_argument(
124
+ "--to",
125
+ choices=["transmittance", "absorbance"],
126
+ default="transmittance",
127
+ help="conversion direction (default: transmittance, i.e. input is "
128
+ "treated as Absorbance)",
129
+ )
130
+ ap.add_argument("--digits", type=int, default=4, help="decimal places on output")
131
+ args = ap.parse_args()
132
+
133
+ header, rows = read_plain_csv(args.csv_in)
134
+ wavenumbers = [wn for wn, _ in rows]
135
+ intensities = [inten for _, inten in rows]
136
+
137
+ converted = convert_series(intensities, to=args.to)
138
+
139
+ write_plain_csv(args.out, header, wavenumbers, converted, digits=args.digits)
140
+
141
+ label = "%Transmittance" if args.to == "transmittance" else "Absorbance"
142
+ print(f"wrote {len(converted)} points to {args.out} ({label})")
143
+
144
+
145
+ if __name__ == "__main__":
146
+ main()
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env python3
2
+ """absorptivity_to_absorbance.py -- NIST QUANT-IR gas-phase .jdx -> true
3
+ Absorbance CSVs, for any report.
4
+
5
+ python3 absorptivity_to_absorbance.py FILE.jdx --name NAME \\
6
+ --out csv/NAME.csv --plain-out plain/NAME.csv (--peak-a A | --cl CL)
7
+
8
+ Self-contained, like this folder's other tools: what a future session needs
9
+ is in this docstring.
10
+
11
+ ===============================================================================
12
+ 1. WHAT THE FILE HOLDS, AND WHAT IT IS NOT
13
+ ===============================================================================
14
+ NIST QUANT-IR gas-phase spectra on the WebBook (##$NIST SOURCE=QUANT-IR, e.g.
15
+ 1634-04-4-IR.jdx, MTBE, seen 2026-10-06) carry ##YUNITS=(micromol/mol)-1m-1
16
+ (base 10): the decadic absorption coefficient per (umol/mol x m), i.e.
17
+
18
+ alpha = A / (c * L) c in umol/mol (mole fraction), L in m
19
+
20
+ so values are ~1e-3 and are NOT an absorbance. jdx_tool.py reads them as
21
+ "Absorbance" (same upward bands, nothing converted) so it can decode the
22
+ file, but they cannot go on an axis next to real spectra, and pgfplots stops
23
+ with "Dimension too large" at that magnitude.
24
+
25
+ ===============================================================================
26
+ 2. THE CONVERSION
27
+ ===============================================================================
28
+ Beer-Lambert, linear in concentration x path:
29
+
30
+ A = alpha * c * L = alpha * CL, CL in umol/mol x m
31
+
32
+ The file does NOT state c or L (it says "Primary Gas Standard" and a multipass
33
+ cell, nothing numeric), and this script never invents them. You must give the
34
+ product, one of two ways:
35
+
36
+ --cl CL the real c x L, umol/mol x m, if you know it from the
37
+ source paper/NIST's record (a true absorbance).
38
+ --peak-a A scale so the strongest band reaches absorbance A (CL =
39
+ A / max alpha). Peak-normalised: the SHAPE and band ratios
40
+ are exact (A is linear in alpha), the absolute height is a
41
+ choice, not a measurement. Say so in the figure caption
42
+ ("scaled to strongest band A = 1").
43
+
44
+ Because the conversion is a single multiplier, band ratios, positions and
45
+ widths are those of the original; only the y scale changes. The archival
46
+ CSV is written too, in Absorbance, through jdx_tool.py's own writers (same
47
+ shapes as any other jdx_tool output). Both CSVs are therefore ready for
48
+ spa_peaks.py --axis Absorbance and \\irspectrum {Absorbance}.
49
+
50
+ Then, as for every Absorbance source, run absorbance_to_transmittance.py on
51
+ the plain CSV: the house convention is %T only. With --peak-a the %T depth
52
+ is as arbitrary as the chosen A, so say so in the caption.
53
+
54
+ Verified 2026-10-06 on 1634-04-4-IR.jdx: output max = the --peak-a value,
55
+ band ratios identical to the input's, plain CSV 650+ cm-1 only.
56
+ """
57
+ import argparse
58
+ import os
59
+ import sys
60
+
61
+ from . import jdx_tool # noqa: E402 (decoder and house CSV writers)
62
+
63
+
64
+ def main():
65
+ ap = argparse.ArgumentParser(
66
+ description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
67
+ )
68
+ ap.add_argument("jdx", help="QUANT-IR .jdx with absorptivity y-units")
69
+ ap.add_argument("--name", required=True, help="NAME_WITH_UNDERSCORES (CSV header)")
70
+ ap.add_argument("--out", required=True, help="archival CSV path")
71
+ ap.add_argument("--plain-out", required=True, help="plain CSV path")
72
+ g = ap.add_mutually_exclusive_group(required=True)
73
+ g.add_argument("--cl", type=float, help="c x L, umol/mol x m (true absorbance)")
74
+ g.add_argument("--peak-a", type=float, help="scale so max band = this absorbance")
75
+ args = ap.parse_args()
76
+
77
+ xs, ys, meta = jdx_tool.read_jdx(args.jdx)
78
+ with open(args.jdx, encoding="utf-8", errors="replace") as f:
79
+ if "(MICROMOL/MOL)-1M-1" not in f.read().upper():
80
+ sys.exit(f"{args.jdx}: y-units are not (micromol/mol)-1m-1; nothing to convert")
81
+ top = max(ys)
82
+ if top <= 0:
83
+ sys.exit("no positive band in the spectrum")
84
+ cl = args.cl if args.cl is not None else args.peak_a / top
85
+ ys = [y * cl for y in ys]
86
+ jdx_tool.write_csv(xs, ys, meta, args.out, args.name)
87
+ jdx_tool.write_plain_csv(xs, ys, meta, args.plain_out)
88
+ how = "c x L given" if args.cl is not None else f"peak-normalised, strongest band A = {args.peak_a}"
89
+ print(f"{args.jdx}: A = alpha x {cl:.6g} ({how}); max A {max(ys):.4f} "
90
+ f"-> {args.out}, {args.plain_out}")
91
+
92
+
93
+ if __name__ == "__main__":
94
+ main()
irspec/cli.py ADDED
@@ -0,0 +1,54 @@
1
+ """The `irspec` command: one entry point over the tools.
2
+
3
+ irspec session DIR a folder of spectra -> CSVs, peaks, figure PDF
4
+ irspec export PDF ... PNG/TIFF/SVG pictures from a figure PDF
5
+ irspec plot DIR ... the figures drawn by matplotlib or R, no TeX needed
6
+ irspec spa ... OMNIC .SPA -> CSVs, peaks, legacy plots (spa_tool)
7
+ irspec jdx ... JCAMP-DX -> CSVs (jdx_tool)
8
+ irspec peaks ... peak-pick a .SPA, .jdx or CSV (spa_peaks)
9
+ irspec digitize ... a spectrum published as a picture -> CSVs (ir_digitize)
10
+ irspec a2t ... Absorbance <-> %Transmittance (absorbance_to_transmittance)
11
+ irspec absorptivity ... QUANT-IR absorptivity -> Absorbance (absorptivity_to_absorbance)
12
+ irspec corr ... Pearson r between spectra (ir_corr)
13
+
14
+ `session` is the command most people want; the others are its steps, for when
15
+ one step is all that is needed (`irspec jdx convert X.jdx`). Each takes its
16
+ own --help.
17
+ """
18
+ import importlib
19
+ import sys
20
+
21
+ from . import __version__
22
+
23
+ COMMANDS = {
24
+ "session": "session",
25
+ "export": "export",
26
+ "plot": "plot",
27
+ "spa": "spa_tool",
28
+ "jdx": "jdx_tool",
29
+ "peaks": "spa_peaks",
30
+ "digitize": "ir_digitize",
31
+ "a2t": "absorbance_to_transmittance",
32
+ "absorptivity": "absorptivity_to_absorbance",
33
+ "corr": "ir_corr",
34
+ }
35
+
36
+
37
+ def main(argv=None):
38
+ argv = list(sys.argv[1:] if argv is None else argv)
39
+ if not argv or argv[0] in ("-h", "--help", "help"):
40
+ print(__doc__.strip())
41
+ return
42
+ if argv[0] in ("-V", "--version"):
43
+ print(f"irspec {__version__}")
44
+ return
45
+ cmd = argv[0]
46
+ if cmd not in COMMANDS:
47
+ sys.exit(f"irspec: unknown command {cmd!r}; choose from: {', '.join(COMMANDS)}")
48
+ mod = importlib.import_module(f"irspec.{COMMANDS[cmd]}")
49
+ sys.argv = [f"irspec {cmd}", *argv[1:]]
50
+ mod.main()
51
+
52
+
53
+ if __name__ == "__main__":
54
+ main()
@@ -0,0 +1,92 @@
1
+ Copyright 2001-2021 The STIX Fonts Project Authors (https://github.com/stipub/stixfonts), with Reserved Font Name "TM Math". STIX Fonts™ is a trademark of The Institute of Electrical and Electronics Engineers, Inc.
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ http://scripts.sil.org/OFL
6
+
7
+ -----------------------------------------------------------
8
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
9
+ -----------------------------------------------------------
10
+
11
+ PREAMBLE
12
+ The goals of the Open Font License (OFL) are to stimulate worldwide
13
+ development of collaborative font projects, to support the font creation
14
+ efforts of academic and linguistic communities, and to provide a free and
15
+ open framework in which fonts may be shared and improved in partnership
16
+ with others.
17
+
18
+ The OFL allows the licensed fonts to be used, studied, modified and
19
+ redistributed freely as long as they are not sold by themselves. The
20
+ fonts, including any derivative works, can be bundled, embedded,
21
+ redistributed and/or sold with any software provided that any reserved
22
+ names are not used by derivative works. The fonts and derivatives,
23
+ however, cannot be released under any other type of license. The
24
+ requirement for fonts to remain under this license does not apply
25
+ to any document created using the fonts or their derivatives.
26
+
27
+ DEFINITIONS
28
+ "Font Software" refers to the set of files released by the Copyright
29
+ Holder(s) under this license and clearly marked as such. This may
30
+ include source files, build scripts and documentation.
31
+
32
+ "Reserved Font Name" refers to any names specified as such after the
33
+ copyright statement(s).
34
+
35
+ "Original Version" refers to the collection of Font Software components as
36
+ distributed by the Copyright Holder(s).
37
+
38
+ "Modified Version" refers to any derivative made by adding to, deleting,
39
+ or substituting -- in part or in whole -- any of the components of the
40
+ Original Version, by changing formats or by porting the Font Software to a
41
+ new environment.
42
+
43
+ "Author" refers to any designer, engineer, programmer, technical
44
+ writer or other person who contributed to the Font Software.
45
+
46
+ PERMISSION & CONDITIONS
47
+ Permission is hereby granted, free of charge, to any person obtaining
48
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
49
+ redistribute, and sell modified and unmodified copies of the Font
50
+ Software, subject to the following conditions:
51
+
52
+ 1) Neither the Font Software nor any of its individual components,
53
+ in Original or Modified Versions, may be sold by itself.
54
+
55
+ 2) Original or Modified Versions of the Font Software may be bundled,
56
+ redistributed and/or sold with any software, provided that each copy
57
+ contains the above copyright notice and this license. These can be
58
+ included either as stand-alone text files, human-readable headers or
59
+ in the appropriate machine-readable metadata fields within text or
60
+ binary files as long as those fields can be easily viewed by the user.
61
+
62
+ 3) No Modified Version of the Font Software may use the Reserved Font
63
+ Name(s) unless explicit written permission is granted by the corresponding
64
+ Copyright Holder. This restriction only applies to the primary font name as
65
+ presented to the users.
66
+
67
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
68
+ Software shall not be used to promote, endorse or advertise any
69
+ Modified Version, except to acknowledge the contribution(s) of the
70
+ Copyright Holder(s) and the Author(s) or with their explicit written
71
+ permission.
72
+
73
+ 5) The Font Software, modified or unmodified, in part or in whole,
74
+ must be distributed entirely under this license, and must not be
75
+ distributed under any other license. The requirement for fonts to
76
+ remain under this license does not apply to any document created
77
+ using the Font Software.
78
+
79
+ TERMINATION
80
+ This license becomes null and void if any of the above conditions are
81
+ not met.
82
+
83
+ DISCLAIMER
84
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
85
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
86
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
87
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
88
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
89
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
90
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
91
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
92
+ OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,10 @@
1
+ STIX Two Text (Regular, Italic) and STIX Two Math, unmodified, from the STIX Fonts
2
+ Project (https://github.com/stipub/stixfonts), version 2.13 (Text) and 2.12 (Math).
3
+ Copyright 2001-2021 The STIX Fonts Project Authors. Licensed under the SIL Open
4
+ Font License, Version 1.1: the full text is in OFL.txt, and the fonts' own license
5
+ fields say the same.
6
+
7
+ irspec uses them only when STIX Two is not installed on the machine: matplotlib and
8
+ R draw with them, and the plain-lualatex fallback loads them from irspec-fonts/.
9
+ They are not sold or distributed on their own; they travel with irspec under the
10
+ same license terms.
Binary file