Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 1 addition & 112 deletions refl1d/probe/data_loaders/load4.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
# third party imports
from bumps.data import parse_multi, strip_quotes
import numpy as np
from orsopy.fileio.orso import load_nexus, load_orso

# refl1d imports
from refl1d.probe import (
Expand All @@ -18,117 +17,7 @@
from refl1d.probe.resolution import QL2T, QT2L, FWHM2sigma, dQdL2dT, dQdT2dLoL, sigma2FWHM
from refl1d.sample.reflectivity import BASE_GUIDE_ANGLE


def parse_orso(filename):
"""
Load an ORSO text (.ort) or binary (.orb) file containing one or more datasets

Parameters
----------
filename : str
The path to the ORSO file to be loaded.

Returns
-------
list of tuple
A list of tuples, each containing a header dictionary and a data array derived from each loaded dataset.
The header dictionary contains metadata about the measurement,
and the data array contains the measurement data.

Notes
-----
The function supports both ORSO text (.ort) and binary (.orb) files.
The polarization information is converted using a predefined mapping.
The header dictionary includes keys for polarization, angle, angular resolution,
wavelength, and wavelength resolution.
"""
if filename.endswith(".ort"):
entries = load_orso(filename)
elif filename.endswith(".orb"):
entries = load_nexus(filename)

POL_CONVERSION = {
"po": "++",
"mo": "--",
"mm": "--",
"mp": "-+",
"pm": "+-",
"pp": "++",
}

entries_out = []
for entry in entries:
header = entry.info
data = entry.data
settings = header.data_source.measurement.instrument_settings
columns = header.columns
polarization = POL_CONVERSION.get(settings.polarization, "unpolarized")
header_out = {"polarization": polarization}

def get_key(orso_name, refl1d_name, refl1d_resolution_name):
"""
Extract value and error from one of the ORSO columns. If no column corresponding
to entry `orso_name` is found, search in the instrument settings.

Parameters
----------
orso_name : str
The name of the ORSO column or instrument setting to extract.
refl1d_name : str
The corresponding refl1d name for the value of entry `orso_name`
refl1d_resolution_name : str
The corresponding refl1d error name the error of entry `orso_name`

Notes
-----
This function requires the instrument setting `orso_name` to have a "magnitue" and "error" attribute.
"""
column_index = next(
(i for i, c in enumerate(columns) if getattr(c, "physical_quantity", None) == orso_name),
None,
)
resolution_index = None
if column_index is not None:
# NOTE: this is based on column being second index (under debate in ORSO)
header_out[refl1d_name] = data[:, column_index]
cname = columns[column_index].name
resolution_index, resolution_column = next(
((i, c) for i, c in enumerate(columns) if getattr(c, "error_of", None) == cname),
(None, None),
)
if resolution_index is not None:
header_out[refl1d_resolution_name] = data[:, resolution_index]
if resolution_column.value_is == "FWHM":
header_out[refl1d_resolution_name] = FWHM2sigma(header_out[refl1d_resolution_name])

# Fall back to instrument_settings if no column found
v = getattr(settings, orso_name, None)
if hasattr(v, "magnitude") and column_index is None:
# only set if not already set from column
header_out[refl1d_name] = v.magnitude
if hasattr(v, "error") and resolution_index is None:
header_out[refl1d_resolution_name] = v.error.error_value
if v.error.value_is == "FWHM":
header_out[refl1d_resolution_name] = float(FWHM2sigma(header_out[refl1d_resolution_name]))

get_key("incident_angle", "angle", "angular_resolution")
get_key("wavelength", "wavelength", "wavelength_resolution")

# convert error columns from FWHM to sigma if they are present
# and need conversion
if len(columns) >= 3:
# third column is always uncertainty on reflectivity
dR_col = columns[2]
if dR_col.value_is == "FWHM":
data[:, 2] = FWHM2sigma(data[:, 2])
if len(columns) >= 4:
# fourth column is always uncertainty on Q
dQ_col = columns[3]
if dQ_col.value_is == "FWHM":
data[:, 3] = FWHM2sigma(data[:, 3])

entries_out.append((header_out, np.array(data).T))
return entries_out
from .orso import parse_orso


def load4(
Expand Down
210 changes: 210 additions & 0 deletions refl1d/probe/data_loaders/orso.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import numpy as np
from orsopy.fileio.orso import load_nexus, load_orso
import orsopy.fileio.model_language as orsopy_model
from orsopy.fileio.model_language import Layer as ORSOLayer, SampleModel as ORSOSample, Material as ORSOMaterial
from orsopy.utils.resolver_slddb import ResolverSLDDB
from refl1d.sample.layers import Stack, Slab
from refl1d.sample.material import Compound, Mixture, BulkDensityMaterial, NumberDensityMaterial, SLD, Vacuum
from refl1d.probe.resolution import FWHM2sigma


def parse_orso(filename):
"""
Load an ORSO text (.ort) or binary (.orb) file containing one or more datasets

Parameters
----------
filename : str
The path to the ORSO file to be loaded.

Returns
-------
list of tuple
A list of tuples, each containing a header dictionary and a data array derived from each loaded dataset.
The header dictionary contains metadata about the measurement,
and the data array contains the measurement data.

Notes
-----
The function supports both ORSO text (.ort) and binary (.orb) files.
The polarization information is converted using a predefined mapping.
The header dictionary includes keys for polarization, angle, angular resolution,
wavelength, and wavelength resolution.
"""
if filename.endswith(".ort"):
entries = load_orso(filename)
elif filename.endswith(".orb"):
entries = load_nexus(filename)

POL_CONVERSION = {
"po": "++",
"mo": "--",
"mm": "--",
"mp": "-+",
"pm": "+-",
"pp": "++",
}

entries_out = []
for entry in entries:
header = entry.info
data = entry.data
settings = header.data_source.measurement.instrument_settings
columns = header.columns
polarization = POL_CONVERSION.get(settings.polarization, "unpolarized")
header_out = {"polarization": polarization}

def get_key(orso_name, refl1d_name, refl1d_resolution_name):
"""
Extract value and error from one of the ORSO columns. If no column corresponding
to entry `orso_name` is found, search in the instrument settings.

Parameters
----------
orso_name : str
The name of the ORSO column or instrument setting to extract.
refl1d_name : str
The corresponding refl1d name for the value of entry `orso_name`
refl1d_resolution_name : str
The corresponding refl1d error name the error of entry `orso_name`

Notes
-----
This function requires the instrument setting `orso_name` to have a "magnitue" and "error" attribute.
"""
column_index = next(
(i for i, c in enumerate(columns) if getattr(c, "physical_quantity", None) == orso_name),
None,
)
resolution_index = None
if column_index is not None:
# NOTE: this is based on column being second index (under debate in ORSO)
header_out[refl1d_name] = data[:, column_index]
cname = columns[column_index].name
resolution_index, resolution_column = next(
((i, c) for i, c in enumerate(columns) if getattr(c, "error_of", None) == cname),
(None, None),
)
if resolution_index is not None:
header_out[refl1d_resolution_name] = data[:, resolution_index]
if resolution_column.value_is == "FWHM":
header_out[refl1d_resolution_name] = FWHM2sigma(header_out[refl1d_resolution_name])

# Fall back to instrument_settings if no column found
v = getattr(settings, orso_name, None)
if hasattr(v, "magnitude") and column_index is None:
# only set if not already set from column
header_out[refl1d_name] = v.magnitude
if hasattr(v, "error") and resolution_index is None:
header_out[refl1d_resolution_name] = v.error.error_value
if v.error.value_is == "FWHM":
header_out[refl1d_resolution_name] = float(FWHM2sigma(header_out[refl1d_resolution_name]))

get_key("incident_angle", "angle", "angular_resolution")
get_key("wavelength", "wavelength", "wavelength_resolution")

# convert error columns from FWHM to sigma if they are present
# and need conversion
if len(columns) >= 3:
# third column is always uncertainty on reflectivity
dR_col = columns[2]
if dR_col.value_is == "FWHM":
data[:, 2] = FWHM2sigma(data[:, 2])
if len(columns) >= 4:
# fourth column is always uncertainty on Q
dQ_col = columns[3]
if dQ_col.value_is == "FWHM":
data[:, 3] = FWHM2sigma(data[:, 3])

entries_out.append((header_out, np.array(data).T))
return entries_out


def orso_samplemodel_converter(model: orsopy_model.SampleModel):
"""
Convert an ORSO sample model to a refl1d Stack model.

Parameters
----------
model : orsopy_model.SampleModel
The ORSO sample model to convert.

Returns
-------
refl1d.sample.layers.Stack
The converted refl1d model.
"""

orso_layers = model.resolve_to_layers()

refl1d_layers = [orso_layer_converter(layer) for layer in orso_layers]

return Stack(refl1d_layers)


def orso_layer_converter(layer: orsopy_model.Layer):
"""
Convert an ORSO layer to a refl1d Slab.

Parameters
----------
layer : ORSOSample.Layer
The ORSO layer to convert.

Returns
-------
refl1d.sample.layers.Slab
The converted refl1d slab.
"""

refl1d_material = orso_material_converter(layer.material)

refl1d_layer = Slab(
material=refl1d_material,
thickness=layer.thickness.as_unit("angstrom"),
interface=layer.roughness.as_unit("angstrom") if layer.roughness else None,
)

return refl1d_layer


def orso_material_converter(material: ORSOMaterial):
"""
Convert an ORSO material to a refl1d Material.

Parameters
----------
material : ORSOMaterial
The ORSO material to convert.

Returns
-------
refl1d.sample.material.Material
The converted refl1d material.
"""
if isinstance(material, orsopy_model.Composit):
parts = []
for component, fraction in material.composition.items():
# TODO: how are we supposed to get the number density from ORSO?
number_density = ResolverSLDDB().resolve_formula(component) # in 1/nm³
cmaterial = NumberDensityMaterial(
formula=component, number_density=number_density * 1e21
) # convert to 1/cm³
parts.extend([cmaterial, fraction * 100]) # convert to percentage for Mixture
# Mixture is expecting a list [base, M2, F2, M3, F3, ...]
# but ORSO Composit does not have a base material,
# so we will set that to vacuum with fraction 0.0 (implicitly)
# as the other fractions add up to 1.0
return Mixture(
base=Vacuum(),
parts=parts,
)
elif material.mass_density is not None:
return BulkDensityMaterial(formula=material.formula, density=material.mass_density.as_unit("g/cm^3"))
elif material.number_density is not None:
return NumberDensityMaterial(formula=material.formula, number_density=material.number_density.as_unit("1/cm^3"))
elif material.sld is not None:
sld_value = material.sld.as_unit("1/angstrom^2") * 1e6 # in 1e-6 A^-2
return SLD(rho=sld_value.real, irho=sld_value.imag)
else:
raise ValueError(f"Unsupported material: {material}")
3 changes: 2 additions & 1 deletion refl1d/sample/material.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ class Vacuum(Scatterer):

def __init__(self, *args, **kw):
self.name = "Vacuum"
self.density = 0.0

def parameters(self):
return []
Expand Down Expand Up @@ -651,7 +652,7 @@ def _density(self):
if (fraction < 0).any():
return nan
volume = self._volume(fraction)
density = np.array([m.density() for m in [self.base] + self.material])
density = np.array([float(m.density) for m in [self.base] + self.material])
return np.sum(volume * density)

density = property(_density, doc=_density.__doc__)
Expand Down
1 change: 1 addition & 0 deletions refl1d/webview/server/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .cli import start_refl1d_server
from bumps.webview.server.cli import BumpsOptions
6 changes: 4 additions & 2 deletions refl1d/webview/server/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
import sys
import asyncio
from pathlib import Path
from typing import Optional

from bumps.webview.server import cli
from bumps.webview.server.cli import BumpsOptions

from . import api # uses side-effects to register refl1d functions
from refl1d import __version__
Expand Down Expand Up @@ -35,7 +37,7 @@ def main():
cli.plugin_main(name="refl1d", client=CLIENT_PATH, version=__version__)


def start_refl1d_server():
def start_refl1d_server(options: Optional[BumpsOptions] = None):
"""
Start a Jupyter server for the webview.
This returns an asyncio.Task object that should be awaited
Expand All @@ -48,7 +50,7 @@ def start_refl1d_server():
api.state.app_version = __version__
api.state.client_path = CLIENT_PATH

return asyncio.create_task(start_app(jupyter_link=True))
return asyncio.create_task(start_app(options, jupyter_link=True))


if __name__ == "__main__":
Expand Down
Loading
Loading