From 4e6d47c9c4887552bcda8437b059179a4c3f15c7 Mon Sep 17 00:00:00 2001 From: bbm Date: Wed, 18 Jun 2025 11:35:03 -0400 Subject: [PATCH 1/9] allow specifying a BumpsOptions() object when starting the webview server from jupyter --- refl1d/webview/server/__init__.py | 1 + refl1d/webview/server/cli.py | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/refl1d/webview/server/__init__.py b/refl1d/webview/server/__init__.py index 94a38144a..90dad7961 100644 --- a/refl1d/webview/server/__init__.py +++ b/refl1d/webview/server/__init__.py @@ -1 +1,2 @@ from .cli import start_refl1d_server +from bumps.webview.server.cli import BumpsOptions diff --git a/refl1d/webview/server/cli.py b/refl1d/webview/server/cli.py index d7fefb0ea..f7e6f4078 100644 --- a/refl1d/webview/server/cli.py +++ b/refl1d/webview/server/cli.py @@ -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__ @@ -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 @@ -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__": From 2d9fc6de547d98e116ac5f206fd0150d377cdf25 Mon Sep 17 00:00:00 2001 From: bbm Date: Wed, 18 Jun 2025 17:28:34 -0400 Subject: [PATCH 2/9] move orso loader to separate module --- refl1d/probe/data_loaders/load4.py | 93 +----------------------------- 1 file changed, 1 insertion(+), 92 deletions(-) diff --git a/refl1d/probe/data_loaders/load4.py b/refl1d/probe/data_loaders/load4.py index 502994990..306a9bb5d 100644 --- a/refl1d/probe/data_loaders/load4.py +++ b/refl1d/probe/data_loaders/load4.py @@ -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 ( @@ -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( From 1820e13e254ad0c2372bbe87c95389e8d1ea721f Mon Sep 17 00:00:00 2001 From: bbm Date: Wed, 18 Jun 2025 17:29:48 -0400 Subject: [PATCH 3/9] adding orso module with translator for orso sample -> refl1d sample --- refl1d/probe/data_loaders/orso.py | 187 ++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 refl1d/probe/data_loaders/orso.py diff --git a/refl1d/probe/data_loaders/orso.py b/refl1d/probe/data_loaders/orso.py new file mode 100644 index 000000000..4c3366766 --- /dev/null +++ b/refl1d/probe/data_loaders/orso.py @@ -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 "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 + + +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}") From bc19c754f5899f2befee153c0a0ddae69dddd61f Mon Sep 17 00:00:00 2001 From: Brian Benjamin Maranville Date: Mon, 23 Jun 2025 14:11:08 -0400 Subject: [PATCH 4/9] Fix spelling --- refl1d/probe/data_loaders/orso.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/refl1d/probe/data_loaders/orso.py b/refl1d/probe/data_loaders/orso.py index 4c3366766..91e695431 100644 --- a/refl1d/probe/data_loaders/orso.py +++ b/refl1d/probe/data_loaders/orso.py @@ -69,7 +69,7 @@ def get_key(orso_name, refl1d_name, refl1d_resolution_name): Notes ----- - This function requires the instrument setting `orso_name` to have a "magnitue" and "error" attribute. + 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), From 8c3932bc4dfc7e1a9f15fd89fb72041e1bf54359 Mon Sep 17 00:00:00 2001 From: bbm Date: Tue, 10 Feb 2026 15:49:31 -0500 Subject: [PATCH 5/9] fixes to support Mixture class (incomplete) --- refl1d/sample/material.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/refl1d/sample/material.py b/refl1d/sample/material.py index 07f2b6164..a40bb9852 100644 --- a/refl1d/sample/material.py +++ b/refl1d/sample/material.py @@ -105,6 +105,7 @@ class Vacuum(Scatterer): def __init__(self, *args, **kw): self.name = "Vacuum" + self.density = 0.0 def parameters(self): return [] @@ -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__) From f9f3e4b18064d6835ba51076326bfeef6fe01e6d Mon Sep 17 00:00:00 2001 From: bbm Date: Tue, 10 Feb 2026 15:50:08 -0500 Subject: [PATCH 6/9] fix units conversions for Mixture class --- refl1d/probe/data_loaders/orso.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/refl1d/probe/data_loaders/orso.py b/refl1d/probe/data_loaders/orso.py index 91e695431..0803a089f 100644 --- a/refl1d/probe/data_loaders/orso.py +++ b/refl1d/probe/data_loaders/orso.py @@ -99,13 +99,13 @@ def get_key(orso_name, refl1d_name, refl1d_resolution_name): return entries_out -def orso_sample_converter(model: orsopy_model.SampleModel): +def orso_samplemodel_converter(model: orsopy_model.SampleModel): """ Convert an ORSO sample model to a refl1d Stack model. Parameters ---------- - model : ORSOSample + model : orsopy_model.SampleModel The ORSO sample model to convert. Returns @@ -166,8 +166,10 @@ def orso_material_converter(material: ORSOMaterial): 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]) + 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) From fc3a7aa60fc080fdbed785f7b165373bfcf0d19f Mon Sep 17 00:00:00 2001 From: bbm Date: Tue, 10 Feb 2026 16:04:15 -0500 Subject: [PATCH 7/9] apply updated changes to parse_orso to the new module --- refl1d/probe/data_loaders/orso.py | 41 +++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/refl1d/probe/data_loaders/orso.py b/refl1d/probe/data_loaders/orso.py index 0803a089f..9327eddff 100644 --- a/refl1d/probe/data_loaders/orso.py +++ b/refl1d/probe/data_loaders/orso.py @@ -5,6 +5,7 @@ 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): @@ -69,32 +70,52 @@ def get_key(orso_name, refl1d_name, refl1d_resolution_name): Notes ----- - This function requires the instrument setting `orso_name` to have a "magnitude" and "error" attribute. + 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 = next( - (i for i, c in enumerate(columns) if getattr(c, "error_of", None) == cname), - None, + 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] - 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 + 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 3ca035c06e5dd656922d985ba020c58196e73d2b Mon Sep 17 00:00:00 2001 From: bbm Date: Tue, 10 Feb 2026 16:06:33 -0500 Subject: [PATCH 8/9] add stub for orsopy sample loader test --- tests/refl1d/test_orsopy_sample.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 tests/refl1d/test_orsopy_sample.py diff --git a/tests/refl1d/test_orsopy_sample.py b/tests/refl1d/test_orsopy_sample.py new file mode 100644 index 000000000..84d0788e9 --- /dev/null +++ b/tests/refl1d/test_orsopy_sample.py @@ -0,0 +1,14 @@ +import yaml +from pathlib import Path +from refl1d.probe import NeutronProbe +from orsopy.fileio.data_source import Sample +from refl1d.probe.data_loaders import orso + +sample_file = Path(__file__).parent / "orso_substacks_sample.yml" +substacks = yaml.safe_load(open(sample_file, "r").read()) + +orso_sample = Sample(name="substacks example", model=substacks) +orso_model = orso_sample.model +refl1d_sample = orso.orso_samplemodel_converter(orso_sample.model) +mixture = refl1d_sample.layers[-1].material +probe = NeutronProbe(T=[0.01], L=[5.0]) From 51f4c7aedce222da346a481ef80527fc2928b836 Mon Sep 17 00:00:00 2001 From: bbm Date: Tue, 10 Feb 2026 16:11:24 -0500 Subject: [PATCH 9/9] adding yml definition file for orso substacks example --- tests/refl1d/orso_substacks_sample.yml | 38 ++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tests/refl1d/orso_substacks_sample.yml diff --git a/tests/refl1d/orso_substacks_sample.yml b/tests/refl1d/orso_substacks_sample.yml new file mode 100644 index 000000000..804a1e724 --- /dev/null +++ b/tests/refl1d/orso_substacks_sample.yml @@ -0,0 +1,38 @@ +stack: substrate | film | water +sub_stacks: + substrate: + sequence: + - material: Si + roughness: 2 + - material: SiO2 + thickness: 5 + roughness: 3 + film: + repetitions: 5 + stack: head_group 4 | tail | tail | head_group 4 +layers: + tail: + material: tailstuff + thickness: 22. +materials: + head_group: + sld: 0.2E-06 + tailstuff: + formula: CH2 + mass_density: 1.2 + SiO2: + formula: SiO2 + mass_density: 2.5 + Si: + formula: Si + mass_density: 2.33 +composits: + water: + composition: + H2O: 0.3 + D2O: 0.7 +globals: + roughness: {magnitude: 5, unit: angstrom} + length_unit: angstrom + mass_density_unit: g/cm^3 + sld_unit: 1/angstrom^2