Skip to content

Commit 5654107

Browse files
authored
Merge pull request #116 from SasView/new_sesans_reader
New sesans reader
2 parents dea648b + fa3434f commit 5654107

11 files changed

Lines changed: 337 additions & 9 deletions

‎sasdata/dataset_types.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ class DatasetType:
3535

3636
sesans = DatasetType(
3737
name="SESANS",
38-
required=["z", "G"],
39-
optional=["stuff", "other stuff", "more stuff"],
38+
required=["SpinEchoLength", "Depolarisation", "Wavelength"],
39+
optional=["Transmission", "Polarisation"],
4040
expected_orders=[["z", "G"]])
4141

4242
dataset_types = {dataset.name for dataset in [one_dim, two_dim, sesans]}

‎sasdata/metadata.py‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,10 @@ class Aperture:
6363
type_: str | None
6464

6565
def summary(self):
66-
return (f"Aperture:\n"
67-
f" Name: {self.name}\n"
68-
f" Aperture size: {self.size}\n"
69-
f" Aperture distance: {self.distance}\n")
66+
return (f" Aperture:\n"
67+
f" Name: {self.name}\n"
68+
f" Aperture size: {self.size}\n"
69+
f" Aperture distance: {self.distance}\n")
7070

7171
@dataclass(kw_only=True)
7272
class Collimation:
@@ -79,10 +79,10 @@ class Collimation:
7979

8080
def summary(self):
8181

82-
#TODO collimation stuff
8382
return (
8483
f"Collimation:\n"
85-
f" Length: {self.length}\n")
84+
f" Length: {self.length}\n"+
85+
"".join([a.summary() for a in self.apertures]))
8686

8787
@dataclass(kw_only=True)
8888
class BeamSize:
@@ -185,7 +185,7 @@ def summary(self):
185185
return (
186186
"\n".join([c.summary() for c in self.collimations]) +
187187
"".join([d.summary() for d in self.detector]) +
188-
self.source.summary())
188+
(self.source.summary() if self.source is not None else ""))
189189

190190
@dataclass(kw_only=True)
191191
class MetaNode:

‎sasdata/temp_sesans_reader.py‎

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
"""
2+
Import SESANS data in SasData format
3+
"""
4+
5+
from sasdata.data import SasData
6+
from sasdata.data_util.loader_exceptions import FileContentsException
7+
from sasdata.dataset_types import sesans
8+
from sasdata.quantities.quantity import Quantity
9+
from sasdata.metadata import (
10+
Metadata,
11+
Sample,
12+
MetaNode,
13+
Process,
14+
)
15+
from sasdata.quantities import unit_parser, units
16+
from collections import defaultdict
17+
from itertools import groupby
18+
import re
19+
import numpy as np
20+
21+
22+
def parse_version(lines: list[str]) -> tuple[str, list[str]]:
23+
header = lines[0]
24+
m = re.search(r"FileFormatVersion\s+(\S+)", header)
25+
26+
if m is None:
27+
raise FileContentsException(
28+
"Sesans file does not contain File Format Version header"
29+
)
30+
31+
return (m.group(0), lines[1:])
32+
33+
34+
def parse_title(kvs: dict[str, str]) -> str:
35+
"""Get the title from the key value store"""
36+
if "Title" in kvs:
37+
return kvs["Title"]
38+
elif "DataFileTitle" in kvs:
39+
return kvs["DataFileTitle"]
40+
for k, v in kvs.items():
41+
if "Title" in k:
42+
return v
43+
return ""
44+
45+
46+
def parse_kvs_quantity(key: str, kvs: dict[str, str]) -> Quantity | None:
47+
if key not in kvs or key + "_unit" not in kvs:
48+
return None
49+
return Quantity(value=float(kvs[key]), units=unit_parser.parse(kvs[key + "_unit"]))
50+
51+
52+
def parse_sample(kvs: dict[str, str]) -> Sample:
53+
"""Get the sample info from the key value store"""
54+
55+
thickness = parse_kvs_quantity("Thickness", kvs)
56+
if thickness is None:
57+
raise FileContentsException(
58+
"SES format must include sample thickness to normalise calculations"
59+
)
60+
61+
return Sample(
62+
name=kvs.get("Sample"),
63+
sample_id=None,
64+
thickness=thickness,
65+
transmission=None,
66+
temperature=None,
67+
position=None,
68+
orientation=None,
69+
details=[],
70+
)
71+
72+
73+
def parse_process(kvs: dict[str, str]) -> Process:
74+
ymax = parse_kvs_quantity("Theta_ymax", kvs)
75+
zmax = parse_kvs_quantity("Theta_zmax", kvs)
76+
orientation = kvs.get("Orientation")
77+
78+
if ymax is None:
79+
raise FileContentsException("SES file must specify Theta_ymax")
80+
if zmax is None:
81+
raise FileContentsException("SES file must specify Theta_zmax")
82+
if orientation is None:
83+
raise FileContentsException("SES file must include encoding orientation")
84+
85+
terms: dict[str, str | Quantity] = {
86+
"ymax": ymax,
87+
"zmax": zmax,
88+
"orientation": orientation,
89+
}
90+
91+
return Process(
92+
name="SESANS Processing",
93+
date=None,
94+
description="Polarisation measurement through a SESANS instrument",
95+
terms=terms,
96+
notes=[],
97+
)
98+
99+
100+
def parse_metanode(kvs: dict[str, str]) -> MetaNode:
101+
"""Convert header into metanode"""
102+
contents: list[MetaNode] = []
103+
title = parse_title(kvs)
104+
105+
for k, v in kvs.items():
106+
if v.endswith("_unit") and v[:-5] in kvs:
107+
# This is the unit for another term
108+
continue
109+
if v + "_unit" in kvs:
110+
contents.append(
111+
MetaNode(
112+
name=k,
113+
attrs={},
114+
contents=Quantity(
115+
value=float(v), units=unit_parser.parse(kvs[k + "_unit"])
116+
),
117+
)
118+
)
119+
else:
120+
contents.append(MetaNode(name=k, attrs={}, contents=v))
121+
122+
return MetaNode(name=title, attrs={}, contents=contents)
123+
124+
125+
def parse_metadata(lines: list[str]) -> tuple[Metadata, dict[str, str], list[str]]:
126+
parts = [
127+
[y for y in x]
128+
for (_, x) in groupby(lines, lambda x: x.startswith("BEGIN_DATA"))
129+
]
130+
131+
if len(parts) != 3:
132+
raise FileContentsException("SES file should have exactly one data section")
133+
134+
# Parse key value store
135+
kvs: dict[str, str] = {}
136+
for line in parts[0]:
137+
m = re.search(r"(\S+)\s+(.+)\n", line)
138+
if not m:
139+
continue
140+
kvs[m.group(1)] = m.group(2)
141+
142+
return (
143+
Metadata(
144+
process=[parse_process(kvs)],
145+
instrument=None,
146+
sample=parse_sample(kvs),
147+
title=parse_title(kvs),
148+
run=[],
149+
definition=None,
150+
raw=parse_metanode(kvs),
151+
),
152+
kvs,
153+
parts[2],
154+
)
155+
156+
157+
def parse_data(lines: list[str], kvs: dict[str, str]) -> dict[str, Quantity]:
158+
159+
data_contents: dict[str, Quantity] = {}
160+
headers = lines[0].split()
161+
points = defaultdict(list)
162+
for line in lines[1:]:
163+
values = line.split()
164+
for idx, v in enumerate(values):
165+
points[headers[idx]].append(float(v))
166+
167+
for h in points.keys():
168+
if h.endswith("_error") and h[:-6] in headers:
169+
# This was an error line
170+
continue
171+
unit = units.none
172+
if h + "_unit" in kvs:
173+
unit = unit_parser.parse(kvs[h + "_unit"])
174+
175+
error = None
176+
if h + "_error" in headers:
177+
error = Quantity(
178+
value=np.asarray(points[h + "_error"]),
179+
units=unit,
180+
)
181+
182+
data_contents[h] = Quantity(
183+
value=np.asarray(points[h]),
184+
units=unit,
185+
standard_error=error,
186+
)
187+
188+
for required in ["SpinEchoLEngth", "Depolarisation", "Wavelength"]:
189+
if required not in data_contents:
190+
raise FileContentsException(f"SES file missing {required}")
191+
192+
return data_contents
193+
194+
195+
def parse_sesans(lines: list[str]) -> SasData:
196+
version, lines = parse_version(lines)
197+
metadata, kvs, lines = parse_metadata(lines)
198+
data_contents = parse_data(lines, kvs)
199+
return SasData(
200+
name="Sesans",
201+
dataset_type=sesans,
202+
data_contents=data_contents,
203+
metadata=metadata,
204+
verbose=False,
205+
)
206+
207+
208+
def load_data(filename) -> SasData:
209+
with open(filename) as infile:
210+
lines = infile.readlines()
211+
return parse_sesans(lines)

‎test/sasdataloader/reference/cansas1d.txt‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ Sample:
4848
Orientation: Rot3(roll=22.5 deg, pitch=0.02 deg, yaw=None)
4949
Collimation:
5050
Length: 123.0 mm
51+
Aperture:
52+
Name: source
53+
Aperture size: Vec3(x=50.0 mm, y=None, z=None)
54+
Aperture distance: 11.0 m
55+
Aperture:
56+
Name: sample
57+
Aperture size: Vec3(x=1.0 mm, y=None, z=None)
58+
Aperture distance: None
5159
Detector:
5260
Name: fictional hybrid
5361
Distance: 4.15 m

‎test/sasdataloader/reference/cansas1d_badunits.txt‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ Sample:
4646
Orientation: Rot3(roll=0.39269908 rad, pitch=0.00034906585 rad, yaw=None)
4747
Collimation:
4848
Length: 0.123 m
49+
Aperture:
50+
Name: source
51+
Aperture size: Vec3(x=50000.0 µm, y=None, z=None)
52+
Aperture distance: 1100.0 cm
53+
Aperture:
54+
Name: sample
55+
Aperture size: Vec3(x=0.1 cm, y=None, z=None)
56+
Aperture distance: None
4957
Detector:
5058
Name: fictional hybrid
5159
Distance: 4.15 m

‎test/sasdataloader/reference/cansas1d_notitle.txt‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ Sample:
4848
Orientation: Rot3(roll=22.5 deg, pitch=0.02 deg, yaw=None)
4949
Collimation:
5050
Length: 123.0 mm
51+
Aperture:
52+
Name: source
53+
Aperture size: Vec3(x=50.0 mm, y=None, z=None)
54+
Aperture distance: 11.0 m
55+
Aperture:
56+
Name: sample
57+
Aperture size: Vec3(x=1.0 mm, y=None, z=None)
58+
Aperture distance: None
5159
Detector:
5260
Name: fictional hybrid
5361
Distance: 4.15 m

‎test/sasdataloader/reference/cansas1d_slit.txt‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ Sample:
4848
Orientation: Rot3(roll=22.5 deg, pitch=0.02 deg, yaw=None)
4949
Collimation:
5050
Length: 123.0 mm
51+
Aperture:
52+
Name: source
53+
Aperture size: Vec3(x=50.0 mm, y=None, z=None)
54+
Aperture distance: 11.0 m
55+
Aperture:
56+
Name: sample
57+
Aperture size: Vec3(x=1.0 mm, y=None, z=None)
58+
Aperture distance: None
5159
Detector:
5260
Name: fictional hybrid
5361
Distance: 4.15 m

‎test/sasdataloader/reference/cansas1d_units.txt‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ Sample:
4646
Orientation: Rot3(roll=0.39269908 rad, pitch=0.00034906585 rad, yaw=None)
4747
Collimation:
4848
Length: 0.123 m
49+
Aperture:
50+
Name: source
51+
Aperture size: Vec3(x=50000.0 µm, y=None, z=None)
52+
Aperture distance: 1100.0 cm
53+
Aperture:
54+
Name: sample
55+
Aperture size: Vec3(x=0.1 cm, y=None, z=None)
56+
Aperture distance: None
4957
Detector:
5058
Name: fictional hybrid
5159
Distance: 4.15 m
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
Sesans
2+
Wavelength
3+
SpinEchoLength
4+
Polarisation
5+
Depolarisation
6+
Metadata:
7+
8+
Polystyrene of Markus Strobl, Full Sine, ++ only, Run: []
9+
========================================================
10+
11+
Definition: Polystyrene of Markus Strobl, Full Sine, ++ only
12+
Process:
13+
Name: SESANS Processing
14+
Date: None
15+
Description: Polarisation measurement through a SESANS instrument
16+
Terms:
17+
ymax: 0.0168 rad
18+
zmax: 0.0168 rad
19+
orientation: Z
20+
Sample:
21+
ID: None
22+
Transmission: None
23+
Thickness: 0.2 cm
24+
Temperature: None
25+
Position: None
26+
Orientation: None
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
Sesans
2+
Wavelength
3+
SpinEchoLength
4+
Depolarisation
5+
Metadata:
6+
7+
PMMA in Mixed Deuterated decalin, Run: []
8+
=======================================
9+
10+
Definition: PMMA in Mixed Deuterated decalin
11+
Process:
12+
Name: SESANS Processing
13+
Date: None
14+
Description: Polarisation measurement through a SESANS instrument
15+
Terms:
16+
ymax: 0.09 rad
17+
zmax: 0.09 rad
18+
orientation: Z
19+
Sample:
20+
ID: None
21+
Transmission: None
22+
Thickness: 2.0 mm
23+
Temperature: None
24+
Position: None
25+
Orientation: None

0 commit comments

Comments
 (0)