mossview 1.0.0__tar.gz

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.
mossview-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jose Luiz Bruiani Barco
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ prune legado
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.4
2
+ Name: mossview
3
+ Version: 1.0.0
4
+ Summary: Um sistema para leitura, processamento e visualização de dados Mössbauer
5
+ Author-email: Seu Nome <seu.email@exemplo.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/seu-usuario/mossview
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Topic :: Scientific/Engineering :: Physics
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: numpy>=1.20.0
17
+ Requires-Dist: matplotlib>=3.3.0
18
+ Dynamic: license-file
19
+
20
+ # MossView
21
+
22
+ MossView is a command-line tool for reading Mössbauer spectra and plotting relative transmission. The main application is [`core-app/mossview.py`](core-app/mossview.py). It reads counts from a `.mos` file and, when it finds an associated `.job` file, uses its metadata for the plot title and velocity scale.
23
+
24
+ ## Requirements
25
+
26
+ - Python 3.8 or later, as declared in `pyproject.toml`.
27
+ - Dependencies listed in [`requirements.txt`](requirements.txt): NumPy and Matplotlib, with their transitive dependencies pinned there.
28
+
29
+ Install the dependencies in a virtual environment if desired:
30
+
31
+ ```bash
32
+ python -m venv .venv
33
+ # Windows PowerShell
34
+ .venv\Scripts\Activate.ps1
35
+ # macOS or Linux
36
+ source .venv/bin/activate
37
+ python -m pip install -r requirements.txt
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ Run from the repository root and pass a `.mos` or `.job` file:
43
+
44
+ ```bash
45
+ python core-app/mossview.py data-control/b025a-26.mos
46
+ python core-app/mossview.py data-control/b025adi.job
47
+ ```
48
+
49
+ When given a `.job`, MossView tries to locate the `.mos` file named near the beginning of the job file, then falls back to a file with the same base name. When given a `.mos`, it looks for a `.job` that references it or has the same base name. Paths are checked as provided, then relative to the script directory.
50
+
51
+ The program prints progress and saves a PNG beside the `.mos` file with the same base name. It also opens an interactive Matplotlib window; close the window to end the process.
52
+
53
+ ## Processing flow
54
+
55
+ 1. Resolves the input path and finds the associated `.mos` and `.job` files.
56
+ 2. Reads recognized metadata from the `.job`'s `&DATA ... &END` block: `VMAX` (absolute value), `ND`, and `TRIANG`. The plot uses `VMAX`, defaulting to 10 mm/s when unavailable. `ND` and `TRIANG` are read but do not affect current processing.
57
+ 3. Reads counts from the `.mos`. The parser accepts XML containing a `<data>` element, as well as text containing numbers. It can read the XML `time` attribute, but does not currently use it in the plot.
58
+ 4. Builds a linear velocity scale from `-VMAX` to `+VMAX`, with one point per count.
59
+ 5. Calculates relative transmission as `count / maximum_count × 100` and plots Poisson error bars as `√count / maximum_count × 100`.
60
+ 6. Saves and displays the plot.
61
+
62
+ ## `&PARAM` block validation
63
+
64
+ When a `.job` file is available, the program examines assignments only inside the `&PARAM ... &END` block. It compares selected parameters with final values found in tables in the same file. It also checks `ARX` values above 0.5 mm/s when no applicable reference is available. If it finds discrepancies, it writes a copy with the `-updated.job` suffix, reports the changes, and uses that copy. The original `.job` is not overwritten by this process.
65
+
66
+ This correction is a heuristic for numeric discrepancies and `ARX` values. It is not a complete NORMOS format validator or a guarantee of physical correctness. Review the `-updated.job` file before using it with another program.
67
+
68
+ ## Repository components
69
+
70
+ | Path | Purpose |
71
+ | --- | --- |
72
+ | `core-app/mossview.py` | Main application: file reading, association, limited validation, and plotting. |
73
+ | `data-control/` | Local sample `.mos`, `.job`, and image files. This directory is ignored by Git. |
74
+ | `legado/main.py` | Independent plotting simulator for damped sine waves and Lissajous curves; it is not called by the current MossView application. |
75
+ | `legado/executables/` | Historical DOS/MBPLOT-related executables, separate from the main application. |
76
+ | `website/` | Minimal website content; it does not provide a working viewer interface. |
77
+ | `pyproject.toml` | Package metadata and Semantic Release configuration. |
78
+ | `.github/workflows/` | Release and PyPI publishing automation. |
79
+
80
+ ## Current scope and limitations
81
+
82
+ - The available interface is a CLI with an interactive plot; there is no dedicated GUI or web application.
83
+ - The velocity axis is always linear and symmetric. The program does not use `ND` or `TRIANG` to reconstruct other acquisition geometries.
84
+ - The permissive text parser extracts numeric tokens but does not support every possible `.mos` format.
85
+ - `pyproject.toml` sets `packages = []` and defines no console command. Run the script directly as shown above; the publishing configuration alone does not provide an installed CLI.
86
+ - `legado/README.md` describes a historical system and its executables, not the current implementation.
87
+
88
+ ## Development and releases
89
+
90
+ Automatic versioning is configured for the `main` and `beta` branches with Python Semantic Release. The publishing workflow builds and uploads the package to PyPI when a GitHub release is published. Packaging configuration and workflows are still evolving; to run the tool locally, use the commands in the Usage section.
91
+
92
+ ## License
93
+
94
+ Distributed under the MIT License. See [`LICENSE`](LICENSE).
@@ -0,0 +1,75 @@
1
+ # MossView
2
+
3
+ MossView is a command-line tool for reading Mössbauer spectra and plotting relative transmission. The main application is [`core-app/mossview.py`](core-app/mossview.py). It reads counts from a `.mos` file and, when it finds an associated `.job` file, uses its metadata for the plot title and velocity scale.
4
+
5
+ ## Requirements
6
+
7
+ - Python 3.8 or later, as declared in `pyproject.toml`.
8
+ - Dependencies listed in [`requirements.txt`](requirements.txt): NumPy and Matplotlib, with their transitive dependencies pinned there.
9
+
10
+ Install the dependencies in a virtual environment if desired:
11
+
12
+ ```bash
13
+ python -m venv .venv
14
+ # Windows PowerShell
15
+ .venv\Scripts\Activate.ps1
16
+ # macOS or Linux
17
+ source .venv/bin/activate
18
+ python -m pip install -r requirements.txt
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ Run from the repository root and pass a `.mos` or `.job` file:
24
+
25
+ ```bash
26
+ python core-app/mossview.py data-control/b025a-26.mos
27
+ python core-app/mossview.py data-control/b025adi.job
28
+ ```
29
+
30
+ When given a `.job`, MossView tries to locate the `.mos` file named near the beginning of the job file, then falls back to a file with the same base name. When given a `.mos`, it looks for a `.job` that references it or has the same base name. Paths are checked as provided, then relative to the script directory.
31
+
32
+ The program prints progress and saves a PNG beside the `.mos` file with the same base name. It also opens an interactive Matplotlib window; close the window to end the process.
33
+
34
+ ## Processing flow
35
+
36
+ 1. Resolves the input path and finds the associated `.mos` and `.job` files.
37
+ 2. Reads recognized metadata from the `.job`'s `&DATA ... &END` block: `VMAX` (absolute value), `ND`, and `TRIANG`. The plot uses `VMAX`, defaulting to 10 mm/s when unavailable. `ND` and `TRIANG` are read but do not affect current processing.
38
+ 3. Reads counts from the `.mos`. The parser accepts XML containing a `<data>` element, as well as text containing numbers. It can read the XML `time` attribute, but does not currently use it in the plot.
39
+ 4. Builds a linear velocity scale from `-VMAX` to `+VMAX`, with one point per count.
40
+ 5. Calculates relative transmission as `count / maximum_count × 100` and plots Poisson error bars as `√count / maximum_count × 100`.
41
+ 6. Saves and displays the plot.
42
+
43
+ ## `&PARAM` block validation
44
+
45
+ When a `.job` file is available, the program examines assignments only inside the `&PARAM ... &END` block. It compares selected parameters with final values found in tables in the same file. It also checks `ARX` values above 0.5 mm/s when no applicable reference is available. If it finds discrepancies, it writes a copy with the `-updated.job` suffix, reports the changes, and uses that copy. The original `.job` is not overwritten by this process.
46
+
47
+ This correction is a heuristic for numeric discrepancies and `ARX` values. It is not a complete NORMOS format validator or a guarantee of physical correctness. Review the `-updated.job` file before using it with another program.
48
+
49
+ ## Repository components
50
+
51
+ | Path | Purpose |
52
+ | --- | --- |
53
+ | `core-app/mossview.py` | Main application: file reading, association, limited validation, and plotting. |
54
+ | `data-control/` | Local sample `.mos`, `.job`, and image files. This directory is ignored by Git. |
55
+ | `legado/main.py` | Independent plotting simulator for damped sine waves and Lissajous curves; it is not called by the current MossView application. |
56
+ | `legado/executables/` | Historical DOS/MBPLOT-related executables, separate from the main application. |
57
+ | `website/` | Minimal website content; it does not provide a working viewer interface. |
58
+ | `pyproject.toml` | Package metadata and Semantic Release configuration. |
59
+ | `.github/workflows/` | Release and PyPI publishing automation. |
60
+
61
+ ## Current scope and limitations
62
+
63
+ - The available interface is a CLI with an interactive plot; there is no dedicated GUI or web application.
64
+ - The velocity axis is always linear and symmetric. The program does not use `ND` or `TRIANG` to reconstruct other acquisition geometries.
65
+ - The permissive text parser extracts numeric tokens but does not support every possible `.mos` format.
66
+ - `pyproject.toml` sets `packages = []` and defines no console command. Run the script directly as shown above; the publishing configuration alone does not provide an installed CLI.
67
+ - `legado/README.md` describes a historical system and its executables, not the current implementation.
68
+
69
+ ## Development and releases
70
+
71
+ Automatic versioning is configured for the `main` and `beta` branches with Python Semantic Release. The publishing workflow builds and uploads the package to PyPI when a GitHub release is published. Packaging configuration and workflows are still evolving; to run the tool locally, use the commands in the Usage section.
72
+
73
+ ## License
74
+
75
+ Distributed under the MIT License. See [`LICENSE`](LICENSE).
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "mossview" # Nome do pacote no PyPI
7
+ version = "1.0.0" # Versão inicial do software
8
+ description = "Um sistema para leitura, processamento e visualização de dados Mössbauer"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Seu Nome", email = "seu.email@exemplo.com" }
14
+ ]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Intended Audience :: Science/Research",
20
+ "Topic :: Scientific/Engineering :: Physics",
21
+ ]
22
+ dependencies = [
23
+ "numpy>=1.20.0",
24
+ "matplotlib>=3.3.0",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/seu-usuario/mossview"
29
+
30
+ # Configuração correta para o padrão src/
31
+ [tool.setuptools.packages.find]
32
+ where = ["src"]
33
+ include = ["mossview*"]
34
+
35
+ # Configuração de automação de release e versionamento
36
+ [tool.semantic_release]
37
+ version_toml = [
38
+ "pyproject.toml:project.version",
39
+ ]
40
+ commit_parser = "conventional"
41
+ build_command = "pip install build && python -m build"
42
+
43
+ [tool.semantic_release.branches.main]
44
+ match = "^main$"
45
+ prerelease = false
46
+
47
+ [tool.semantic_release.branches.beta]
48
+ match = "^beta$"
49
+ prerelease = true
50
+ prerelease_token = "beta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,400 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ MossView - Mössbauer spectrum viewer (.mos and .job)
5
+ with validation and filtering restricted to parameters in the &PARAM block.
6
+ """
7
+
8
+ import sys
9
+ import os
10
+ import re
11
+ import argparse
12
+ from pathlib import Path
13
+ import xml.etree.ElementTree as ET
14
+ import numpy as np
15
+ import matplotlib.pyplot as plt
16
+
17
+
18
+ def resolve_file_path(input_path_str: str) -> Path:
19
+ """
20
+ Resolve the input file path using these rules:
21
+ 1. If the absolute path exists, return it.
22
+ 2. If the path exists in the current working directory (CWD), return it.
23
+ 3. If the path exists in the Python script directory, return it.
24
+ """
25
+ path = Path(input_path_str)
26
+
27
+ if path.is_absolute() and path.exists():
28
+ return path.resolve()
29
+
30
+ if path.exists():
31
+ return path.resolve()
32
+
33
+ script_dir = Path(__file__).parent.resolve()
34
+ script_relative_path = script_dir / input_path_str
35
+ if script_relative_path.exists():
36
+ return script_relative_path.resolve()
37
+
38
+ return path.resolve()
39
+
40
+
41
+ def parse_job_file(job_path: Path) -> dict:
42
+ """
43
+ Read a NORMOS .JOB file and extract display metadata and raw content
44
+ for analysis of the &PARAM block.
45
+ """
46
+ info = {
47
+ 'mos_filename': None,
48
+ 'title': '',
49
+ 'vmax': 10.0,
50
+ 'nd': None,
51
+ 'triang': True,
52
+ 'raw_content': ''
53
+ }
54
+
55
+ try:
56
+ with open(job_path, 'r', encoding='utf-8', errors='ignore') as f:
57
+ content = f.read()
58
+ info['raw_content'] = content
59
+ lines = [line.strip() for line in content.splitlines() if line.strip()]
60
+
61
+ for line in lines[:6]:
62
+ if line.lower().endswith('.mos'):
63
+ info['mos_filename'] = line
64
+ break
65
+
66
+ title_lines = []
67
+ for line in lines:
68
+ if line.startswith('*'):
69
+ cleaned = line.strip('*').strip()
70
+ if cleaned:
71
+ title_lines.append(cleaned)
72
+ if title_lines:
73
+ info['title'] = " | ".join(title_lines)
74
+
75
+ data_match = re.search(r'&DATA(.*?)&END', content, re.DOTALL | re.IGNORECASE)
76
+ if data_match:
77
+ data_block = data_match.group(1)
78
+
79
+ vmax_m = re.search(r'VMAX\s*=\s*([+-]?\d+\.?\d*)', data_block, re.IGNORECASE)
80
+ if vmax_m:
81
+ info['vmax'] = abs(float(vmax_m.group(1)))
82
+
83
+ nd_m = re.search(r'ND\s*=\s*(\d+)', data_block, re.IGNORECASE)
84
+ if nd_m:
85
+ info['nd'] = int(nd_m.group(1))
86
+
87
+ triang_m = re.search(r'TRIANG\s*=\s*\.?(TRUE|FALSE)\.?', data_block, re.IGNORECASE)
88
+ if triang_m:
89
+ info['triang'] = (triang_m.group(1).upper() == 'TRUE')
90
+
91
+ except Exception as e:
92
+ print(f"[WARNING] Error reading metadata from .JOB file: {e}")
93
+
94
+ return info
95
+
96
+
97
+ def extract_reference_values(content: str) -> dict:
98
+ """
99
+ Extract reference/final values from report tables in the .JOB/.RES file.
100
+ """
101
+ refs = {}
102
+
103
+ # 1. Parameter table: "Index Name Initial Final Error"
104
+ pattern_index = re.compile(
105
+ r'^\s*\d+\s+([A-Z]+)\s+[+-]?\d+\.?\d*(?:[eE][+-]?\d+)?\s+([+-]?\d+\.?\d*(?:[eE][+-]?\d+)?)',
106
+ re.MULTILINE
107
+ )
108
+ for match in pattern_index.finditer(content):
109
+ param_name = match.group(1).upper()
110
+ try:
111
+ refs[param_name] = float(match.group(2))
112
+ except ValueError:
113
+ pass
114
+
115
+ # 2. "Sub. #Lines Area (mm/s)" table for mapping subspectrum areas (ARX)
116
+ if "Sub. #Lines" in content:
117
+ sub_block = content.split("Sub. #Lines")[1]
118
+ pattern_area = re.compile(r'^\s*(\d+)\s+\d+\s+([+-]?\d+\.?\d*(?:[eE][+-]?\d+)?)', re.MULTILINE)
119
+ for match in pattern_area.finditer(sub_block):
120
+ sub_idx = match.group(1)
121
+ try:
122
+ refs[f"ARX({sub_idx})"] = float(match.group(2))
123
+ except ValueError:
124
+ pass
125
+
126
+ return refs
127
+
128
+
129
+ def validate_and_filter_param_block(job_path: Path, content: str) -> tuple[str, Path, list[str]]:
130
+ """
131
+ Validate and edit ONLY data inside the &PARAM ... &END block.
132
+ Check numeric discrepancies (such as decimal-place shifts in ARX)
133
+ against report data or physical limits.
134
+ """
135
+ param_match = re.search(r'(&PARAM\b.*?&END)', content, re.DOTALL | re.IGNORECASE)
136
+ if not param_match:
137
+ return content, job_path, []
138
+
139
+ original_param_block = param_match.group(1)
140
+ warnings = []
141
+ refs = extract_reference_values(content)
142
+
143
+ # Regular expression to capture KEY(INDEX)=VALUE assignments
144
+ param_kv_pattern = re.compile(r'([A-Z0-9_]+\(\d+\))\s*=\s*([+-]?\d+\.?\d*(?:[eE][+-]?\d+)?)', re.IGNORECASE)
145
+
146
+ def replace_param(match):
147
+ key = match.group(1).upper()
148
+ raw_val = match.group(2)
149
+ try:
150
+ val = float(raw_val)
151
+ except ValueError:
152
+ return match.group(0)
153
+
154
+ base_name = re.sub(r'\(\d+\)', '', key)
155
+ target_val = refs.get(key) or refs.get(base_name)
156
+
157
+ if target_val is not None:
158
+ # Check for a significant deviation from the expected value (e.g. a decimal-place error)
159
+ if abs(val - target_val) > 1e-4:
160
+ formatted_target = f"{target_val:.5f}".rstrip('0').rstrip('.') if abs(target_val) < 100 else f"{target_val}"
161
+ warnings.append(
162
+ f"Parameter {key}={val} is outside the expected range. Corrected to {key}={formatted_target} (expected: {target_val})."
163
+ )
164
+ return match.group(0).replace(raw_val, formatted_target)
165
+
166
+ # Fallback safeguard if ARX exceeds the typical limit (> 0.5 mm/s) without an available table
167
+ if base_name == "ARX" and val > 0.5:
168
+ corrected_val = val / 10.0
169
+ formatted_target = f"{corrected_val:.5f}".rstrip('0').rstrip('.')
170
+ warnings.append(
171
+ f"Parameter {key}={val} exceeds the expected spectrum limit. Corrected to {key}={formatted_target}."
172
+ )
173
+ return match.group(0).replace(raw_val, formatted_target)
174
+
175
+ return match.group(0)
176
+
177
+ new_param_block = param_kv_pattern.sub(replace_param, original_param_block)
178
+
179
+ if warnings and job_path:
180
+ new_content = content.replace(original_param_block, new_param_block)
181
+ stem = job_path.stem
182
+ updated_job_path = job_path if stem.endswith("-updated") else job_path.parent / f"{stem}-updated.job"
183
+
184
+ with open(updated_job_path, 'w', encoding='utf-8') as f:
185
+ f.write(new_content)
186
+
187
+ return new_content, updated_job_path, warnings
188
+
189
+ return content, job_path, warnings
190
+
191
+
192
+ def parse_mos_file(mos_path: Path) -> tuple[np.ndarray, dict]:
193
+ """
194
+ Read counts from a .MOS file in XML (WisSoft) or plain text/ASCII format.
195
+ """
196
+ counts = []
197
+ meta = {}
198
+
199
+ try:
200
+ tree = ET.parse(mos_path)
201
+ root = tree.getroot()
202
+ data_elem = root.find('data')
203
+ if data_elem is not None and data_elem.text:
204
+ text_data = data_elem.text.strip()
205
+ counts = [float(val) for val in text_data.split()]
206
+ if 'time' in data_elem.attrib:
207
+ meta['time'] = float(data_elem.attrib['time'])
208
+ return np.array(counts, dtype=float), meta
209
+ except Exception:
210
+ pass
211
+
212
+ with open(mos_path, 'r', encoding='utf-8', errors='ignore') as f:
213
+ content = f.read()
214
+
215
+ data_match = re.search(r'<data[^>]*>(.*?)</data>', content, re.DOTALL | re.IGNORECASE)
216
+ if data_match:
217
+ text_data = data_match.group(1).strip()
218
+ tokens = text_data.split()
219
+ counts = [float(v) for v in tokens if v.replace('.', '', 1).lstrip('-').isdigit()]
220
+ return np.array(counts, dtype=float), meta
221
+
222
+ tokens = content.split()
223
+ for t in tokens:
224
+ try:
225
+ counts.append(float(t))
226
+ except ValueError:
227
+ continue
228
+
229
+ if not counts:
230
+ raise ValueError(f"Could not extract numeric data from file {mos_path.name}")
231
+
232
+ return np.array(counts, dtype=float), meta
233
+
234
+
235
+ def locate_associated_files(target_path: Path):
236
+ """
237
+ Identify the .MOS and .JOB files from the path provided by the user.
238
+ """
239
+ job_path = None
240
+ mos_path = None
241
+ job_info = {}
242
+
243
+ if target_path.suffix.lower() == '.job':
244
+ job_path = target_path
245
+ job_info = parse_job_file(job_path)
246
+
247
+ if job_info.get('mos_filename'):
248
+ candidate = job_path.parent / job_info['mos_filename']
249
+ if candidate.exists():
250
+ mos_path = candidate
251
+ else:
252
+ for f in job_path.parent.iterdir():
253
+ if f.name.lower() == job_info['mos_filename'].lower():
254
+ mos_path = f
255
+ break
256
+
257
+ if not mos_path:
258
+ candidate = job_path.with_suffix('.MOS')
259
+ if candidate.exists():
260
+ mos_path = candidate
261
+ else:
262
+ candidate_lower = job_path.with_suffix('.mos')
263
+ if candidate_lower.exists():
264
+ mos_path = candidate_lower
265
+
266
+ elif target_path.suffix.lower() == '.mos':
267
+ mos_path = target_path
268
+
269
+ possible_jobs = list(mos_path.parent.glob('*.JOB')) + list(mos_path.parent.glob('*.job'))
270
+ for j_file in possible_jobs:
271
+ j_info = parse_job_file(j_file)
272
+ if j_info.get('mos_filename') and j_info['mos_filename'].lower() == mos_path.name.lower():
273
+ job_path = j_file
274
+ job_info = j_info
275
+ break
276
+
277
+ if not job_path:
278
+ candidate = mos_path.with_suffix('.JOB')
279
+ if candidate.exists():
280
+ job_path = candidate
281
+ job_info = parse_job_file(job_path)
282
+
283
+ return mos_path, job_path, job_info
284
+
285
+
286
+ def calculate_velocity_scale(num_channels: int, vmax: float) -> np.ndarray:
287
+ """
288
+ Calculate the velocity scale (in mm/s) for a linear Mössbauer scan.
289
+ """
290
+ return np.linspace(-abs(vmax), abs(vmax), num_channels)
291
+
292
+
293
+ def plot_mossbauer(velocity: np.ndarray, counts: np.ndarray, title: str, job_name: str, mos_name: str, output_image_path: Path):
294
+ """
295
+ Generate a Mössbauer spectrum plot in relative transmission (%) and raw counts.
296
+ """
297
+ baseline = np.max(counts)
298
+ rel_transmission = (counts / baseline) * 100.0
299
+ errors_rel = (np.sqrt(counts) / baseline) * 100.0
300
+
301
+ plt.style.use('seaborn-v0_8-whitegrid' if 'seaborn-v0_8-whitegrid' in plt.style.available else 'default')
302
+ fig, ax1 = plt.subplots(figsize=(10, 6), dpi=120)
303
+
304
+ ax1.errorbar(velocity, rel_transmission, yerr=errors_rel, fmt='o', color='#004080',
305
+ ecolor='#80b3ff', elinewidth=0.8, markersize=3, label='Experimental Data', alpha=0.85)
306
+
307
+ ax1.axhline(100.0, color='crimson', linestyle='--', linewidth=1.0, label='Baseline (100%)')
308
+
309
+ ax1.set_xlabel('Velocity (mm/s)', fontsize=12, fontweight='bold')
310
+ ax1.set_ylabel('Relative Transmission (%)', fontsize=12, fontweight='bold', color='#004080')
311
+ ax1.tick_params(axis='y', labelcolor='#004080')
312
+
313
+ ax2 = ax1.twinx()
314
+ ax2.set_ylabel('Total Counts', fontsize=11, color='#555555')
315
+ min_counts = np.min(counts)
316
+ ax2.set_ylim((min_counts / baseline) * 100, 100.5)
317
+ ax2.set_yticks(np.linspace((min_counts / baseline) * 100, 100, 6))
318
+ ax2.set_yticklabels([f"{int(val * baseline / 100):,}" for val in ax2.get_yticks()])
319
+ ax2.grid(False)
320
+
321
+ display_title = title if title else f"Mossbauer Spectrum - {mos_name}"
322
+ plt.title(display_title, fontsize=13, fontweight='bold', pad=15)
323
+
324
+ info_text = (f"File: {mos_name}\n"
325
+ f"JOB: {job_name if job_name else 'None'}\n"
326
+ f"Channels: {len(counts)}\n"
327
+ f"Vmax: {np.max(np.abs(velocity)):.4f} mm/s\n"
328
+ f"Max Count: {int(baseline):,}")
329
+
330
+ ax1.text(0.02, 0.05, info_text, transform=ax1.transAxes, fontsize=9,
331
+ verticalalignment='bottom', bbox=dict(boxstyle='round,pad=0.5', facecolor='white', alpha=0.85, edgecolor='#ccc'))
332
+
333
+ ax1.legend(loc='upper right', frameon=True)
334
+ fig.tight_layout()
335
+
336
+ plt.savefig(output_image_path, dpi=300)
337
+ print(f"[SUCCESS] Plot saved to: {output_image_path}")
338
+
339
+ plt.show()
340
+
341
+
342
+ def main():
343
+ parser = argparse.ArgumentParser(
344
+ description="MossView - Mössbauer spectrum reader and viewer (.MOS and .JOB)"
345
+ )
346
+ parser.add_argument("file", help="File name or full path (.job or .mos)")
347
+ args = parser.parse_args()
348
+
349
+ try:
350
+ resolved_path = resolve_file_path(args.file)
351
+ except Exception as e:
352
+ print(f"[ERROR] Failed to process the specified path: {e}")
353
+ sys.exit(1)
354
+
355
+ if not resolved_path.exists():
356
+ print(f"[ERROR] File not found:\n -> {resolved_path}")
357
+ sys.exit(1)
358
+
359
+ print(f"[OK] File found: {resolved_path}")
360
+
361
+ mos_path, job_path, job_info = locate_associated_files(resolved_path)
362
+
363
+ if not mos_path or not mos_path.exists():
364
+ print(f"[ERROR] Could not find the .MOS data file associated with {resolved_path.name}")
365
+ sys.exit(1)
366
+
367
+ # 1. Read and validate the &PARAM block in the .JOB file
368
+ if job_path and job_path.exists():
369
+ raw_content = job_info.get('raw_content', '')
370
+ _, updated_job_path, warnings = validate_and_filter_param_block(job_path, raw_content)
371
+
372
+ if warnings:
373
+ print("\n[WARNING] Values outside the expected range were found in the &PARAM block:")
374
+ for w in warnings:
375
+ print(f" -> {w}")
376
+ print(f"[OK] Updated file with corrected &PARAM block saved to: {updated_job_path}\n")
377
+ job_path = updated_job_path
378
+ job_info = parse_job_file(job_path)
379
+
380
+ # 2. Read the .MOS data file
381
+ try:
382
+ counts, meta = parse_mos_file(mos_path)
383
+ print(f"[OK] .MOS spectrum loaded: {len(counts)} channels read.")
384
+ except Exception as e:
385
+ print(f"[ERROR] Failed to read .MOS file: {e}")
386
+ sys.exit(1)
387
+
388
+ vmax = job_info.get('vmax', 10.0)
389
+ title = job_info.get('title', '')
390
+ velocity = calculate_velocity_scale(len(counts), vmax)
391
+
392
+ job_name = job_path.name if job_path else "None"
393
+ mos_name = mos_path.name
394
+ output_png = mos_path.with_suffix('.png')
395
+
396
+ plot_mossbauer(velocity, counts, title, job_name, mos_name, output_png)
397
+
398
+
399
+ if __name__ == "__main__":
400
+ main()
File without changes
@@ -0,0 +1,76 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+
4
+ class MBPlotSimulator:
5
+ """
6
+ Simulador da lógica de amostragem, transformação de coordenadas
7
+ e geração de gráficos do sistema clássico MBPLOT.
8
+ """
9
+ def __init__(self, screen_width=800, screen_height=600):
10
+ self.screen_width = screen_width
11
+ self.screen_height = screen_height
12
+
13
+ def world_to_screen(self, x_world, y_world, x_min, x_max, y_min, y_max):
14
+ """
15
+ Converte coordenadas de mundo (matemáticas) para coordenadas de tela (pixels).
16
+ """
17
+ x_screen = ((x_world - x_min) / (x_max - x_min)) * self.screen_width
18
+ y_screen = self.screen_height - ((y_world - y_min) / (y_max - y_min)) * self.screen_height
19
+ return x_screen, y_screen
20
+
21
+ def generate_damped_sine(self, x_min=0.0, x_max=10.0, num_points=500, alpha=0.5, omega=2.0 * np.pi):
22
+ """
23
+ Gera a função de onda amortecida: f(x) = exp(-alpha * x) * sin(omega * x)
24
+ """
25
+ x = np.linspace(x_min, x_max, num_points)
26
+ y = np.exp(-alpha * x) * np.sin(omega * x)
27
+ return x, y
28
+
29
+ def generate_lissajous(self, a=3, b=4, delta=np.pi/2, num_points=1000):
30
+ """
31
+ Gera curvas paramétricas de Lissajous: x(t) = sin(a*t + delta), y(t) = sin(b*t)
32
+ """
33
+ t = np.linspace(0, 2 * np.pi, num_points)
34
+ x = np.sin(a * t + delta)
35
+ y = np.sin(b * t)
36
+ return x, y
37
+
38
+ def plot_function(self, x, y, title="MBPLOT - Visualização de Função"):
39
+ """
40
+ Plota o gráfico com estilo retrô similar ao ambiente gráfico DOS.
41
+ """
42
+ x_min, x_max = np.min(x), np.max(x)
43
+ y_min, y_max = np.min(y), np.max(y)
44
+
45
+ # Mapeamento para resolução de tela simulada
46
+ x_pixel, y_pixel = self.world_to_screen(x, y, x_min, x_max, y_min, y_max)
47
+
48
+ plt.figure(figsize=(9, 6), facecolor='black')
49
+ ax = plt.axes()
50
+ ax.set_facecolor('black')
51
+
52
+ # Desenhar linha estilo monitor VGA clássico (Verde / Ciano)
53
+ plt.plot(x_pixel, y_pixel, color='#00FF66', linewidth=1.5, label='Sinal Processado')
54
+
55
+ plt.title(title, color='white', fontsize=12, fontweight='bold')
56
+ plt.xlabel("Pixel X (Viewport)", color='white')
57
+ plt.ylabel("Pixel Y (Viewport Invertido)", color='white')
58
+ plt.tick_params(colors='white')
59
+ plt.grid(True, color='#333333', linestyle='--')
60
+ plt.xlim(0, self.screen_width)
61
+ plt.ylim(self.screen_height, 0) # Invertido igual coordenada de tela
62
+ plt.legend(facecolor='black', edgecolor='white', labelcolor='white')
63
+
64
+ plt.tight_layout()
65
+ plt.show()
66
+
67
+ if __name__ == "__main__":
68
+ simulator = MBPlotSimulator(screen_width=640, screen_height=480)
69
+
70
+ # 1. Plotar Onda Senoidal Amortecida
71
+ x_func, y_func = simulator.generate_damped_sine(x_min=0, x_max=5, num_points=400, alpha=0.6, omega=4.0*np.pi)
72
+ simulator.plot_function(x_func, y_func, title="MBPLOT Simulação: Onda Amortecida f(x) = e^(-a*x) * sin(w*x)")
73
+
74
+ # 2. Plotar Figura de Lissajous
75
+ x_liss, y_liss = simulator.generate_lissajous(a=5, b=6, delta=np.pi/4)
76
+ simulator.plot_function(x_liss, y_liss, title="MBPLOT Simulação: Curva Paramétrica de Lissajous")
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.4
2
+ Name: mossview
3
+ Version: 1.0.0
4
+ Summary: Um sistema para leitura, processamento e visualização de dados Mössbauer
5
+ Author-email: Seu Nome <seu.email@exemplo.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/seu-usuario/mossview
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Topic :: Scientific/Engineering :: Physics
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: numpy>=1.20.0
17
+ Requires-Dist: matplotlib>=3.3.0
18
+ Dynamic: license-file
19
+
20
+ # MossView
21
+
22
+ MossView is a command-line tool for reading Mössbauer spectra and plotting relative transmission. The main application is [`core-app/mossview.py`](core-app/mossview.py). It reads counts from a `.mos` file and, when it finds an associated `.job` file, uses its metadata for the plot title and velocity scale.
23
+
24
+ ## Requirements
25
+
26
+ - Python 3.8 or later, as declared in `pyproject.toml`.
27
+ - Dependencies listed in [`requirements.txt`](requirements.txt): NumPy and Matplotlib, with their transitive dependencies pinned there.
28
+
29
+ Install the dependencies in a virtual environment if desired:
30
+
31
+ ```bash
32
+ python -m venv .venv
33
+ # Windows PowerShell
34
+ .venv\Scripts\Activate.ps1
35
+ # macOS or Linux
36
+ source .venv/bin/activate
37
+ python -m pip install -r requirements.txt
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ Run from the repository root and pass a `.mos` or `.job` file:
43
+
44
+ ```bash
45
+ python core-app/mossview.py data-control/b025a-26.mos
46
+ python core-app/mossview.py data-control/b025adi.job
47
+ ```
48
+
49
+ When given a `.job`, MossView tries to locate the `.mos` file named near the beginning of the job file, then falls back to a file with the same base name. When given a `.mos`, it looks for a `.job` that references it or has the same base name. Paths are checked as provided, then relative to the script directory.
50
+
51
+ The program prints progress and saves a PNG beside the `.mos` file with the same base name. It also opens an interactive Matplotlib window; close the window to end the process.
52
+
53
+ ## Processing flow
54
+
55
+ 1. Resolves the input path and finds the associated `.mos` and `.job` files.
56
+ 2. Reads recognized metadata from the `.job`'s `&DATA ... &END` block: `VMAX` (absolute value), `ND`, and `TRIANG`. The plot uses `VMAX`, defaulting to 10 mm/s when unavailable. `ND` and `TRIANG` are read but do not affect current processing.
57
+ 3. Reads counts from the `.mos`. The parser accepts XML containing a `<data>` element, as well as text containing numbers. It can read the XML `time` attribute, but does not currently use it in the plot.
58
+ 4. Builds a linear velocity scale from `-VMAX` to `+VMAX`, with one point per count.
59
+ 5. Calculates relative transmission as `count / maximum_count × 100` and plots Poisson error bars as `√count / maximum_count × 100`.
60
+ 6. Saves and displays the plot.
61
+
62
+ ## `&PARAM` block validation
63
+
64
+ When a `.job` file is available, the program examines assignments only inside the `&PARAM ... &END` block. It compares selected parameters with final values found in tables in the same file. It also checks `ARX` values above 0.5 mm/s when no applicable reference is available. If it finds discrepancies, it writes a copy with the `-updated.job` suffix, reports the changes, and uses that copy. The original `.job` is not overwritten by this process.
65
+
66
+ This correction is a heuristic for numeric discrepancies and `ARX` values. It is not a complete NORMOS format validator or a guarantee of physical correctness. Review the `-updated.job` file before using it with another program.
67
+
68
+ ## Repository components
69
+
70
+ | Path | Purpose |
71
+ | --- | --- |
72
+ | `core-app/mossview.py` | Main application: file reading, association, limited validation, and plotting. |
73
+ | `data-control/` | Local sample `.mos`, `.job`, and image files. This directory is ignored by Git. |
74
+ | `legado/main.py` | Independent plotting simulator for damped sine waves and Lissajous curves; it is not called by the current MossView application. |
75
+ | `legado/executables/` | Historical DOS/MBPLOT-related executables, separate from the main application. |
76
+ | `website/` | Minimal website content; it does not provide a working viewer interface. |
77
+ | `pyproject.toml` | Package metadata and Semantic Release configuration. |
78
+ | `.github/workflows/` | Release and PyPI publishing automation. |
79
+
80
+ ## Current scope and limitations
81
+
82
+ - The available interface is a CLI with an interactive plot; there is no dedicated GUI or web application.
83
+ - The velocity axis is always linear and symmetric. The program does not use `ND` or `TRIANG` to reconstruct other acquisition geometries.
84
+ - The permissive text parser extracts numeric tokens but does not support every possible `.mos` format.
85
+ - `pyproject.toml` sets `packages = []` and defines no console command. Run the script directly as shown above; the publishing configuration alone does not provide an installed CLI.
86
+ - `legado/README.md` describes a historical system and its executables, not the current implementation.
87
+
88
+ ## Development and releases
89
+
90
+ Automatic versioning is configured for the `main` and `beta` branches with Python Semantic Release. The publishing workflow builds and uploads the package to PyPI when a GitHub release is published. Packaging configuration and workflows are still evolving; to run the tool locally, use the commands in the Usage section.
91
+
92
+ ## License
93
+
94
+ Distributed under the MIT License. See [`LICENSE`](LICENSE).
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ pyproject.toml
5
+ src/mossview/cli.py
6
+ src/mossview/gui.py
7
+ src/mossview.egg-info/PKG-INFO
8
+ src/mossview.egg-info/SOURCES.txt
9
+ src/mossview.egg-info/dependency_links.txt
10
+ src/mossview.egg-info/requires.txt
11
+ src/mossview.egg-info/top_level.txt
12
+ src/mossview/legado/main.py
@@ -0,0 +1,2 @@
1
+ numpy>=1.20.0
2
+ matplotlib>=3.3.0
@@ -0,0 +1 @@
1
+ mossview