Skip to content
Draft
93 changes: 1 addition & 92 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,97 +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,
)
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 = next(
(i for i, c in enumerate(columns) if getattr(c, "error_of", None) == cname),
None,
)
if resolution_index is not None:
header_out[refl1d_resolution_name] = data[:, resolution_index]
else:
v = getattr(settings, orso_name, None)
if hasattr(v, "magnitude"):
header_out[refl1d_name] = v.magnitude
if hasattr(v, "error"):
header_out[refl1d_resolution_name] = v.error.error_value

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

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


def load4(
Expand Down
187 changes: 187 additions & 0 deletions refl1d/probe/data_loaders/orso.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
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


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 "magnitude" and "error" attribute.
"""
column_index = next(
(i for i, c in enumerate(columns) if getattr(c, "physical_quantity", None) == orso_name),
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 = next(
(i for i, c in enumerate(columns) if getattr(c, "error_of", None) == cname),
None,
)
if resolution_index is not None:
header_out[refl1d_resolution_name] = data[:, resolution_index]
else:
v = getattr(settings, orso_name, None)
if hasattr(v, "magnitude"):
header_out[refl1d_name] = v.magnitude
if hasattr(v, "error"):
header_out[refl1d_resolution_name] = v.error.error_value

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

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


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

Parameters
----------
model : ORSOSample
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)
parts.extend([cmaterial, fraction])
# 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}")
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 @@ -4,8 +4,10 @@

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 All @@ -24,7 +26,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 @@ -37,7 +39,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