Skip to content
Merged
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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@
assembly: thickness and roughness) and the subphase (last layer of the last
assembly: thickness) are locked, the table shows exactly what the fit uses,
and the lock is set again after every layer change.
- Adding a layer or an assembly to a project that has no `Si` material still
creates one for the new layer, and the Materials table now shows it at once
instead of after the next unrelated refresh.
- Bayesian results: the marginal-distribution plot is rendered together with the
corner and trace plots when sampling finishes, so its tab no longer renders on
first open, and a plot that failed to render is not retried on every redraw.
- Analysis parameter filters: **model** and **experiment** now go by what owns a
parameter. Only a model's scale and background are experiment parameters;
before, any parameter whose name or group contained words such as `scale` or
`background` (for example one of a material named "Background Material") was
counted as one. A parameter that is only named `thickness` or `roughness`,
outside a layer, is no longer labelled with its assembly's name.
- Source cleanup : duplicated
code and dead code removed (one shared plot-control group for the Experiment,
Sample and Analysis pages, one definition of the experiment order), typed
Expand Down
24 changes: 12 additions & 12 deletions EasyReflectometryApp/Backends/Mock/Sample.qml
Original file line number Diff line number Diff line change
Expand Up @@ -226,36 +226,33 @@ QtObject {
'material': 'label 1',
'solvent': 'label 1',
'thickness': '1',
'thickness_enabled': 'True',
'thickness_enabled': true,
'roughness': '1',
'roughness_enabled': 'True',
'roughness_enabled': true,
'solvation': '1',
'apm': '1',
'apm_enabled': 'True',
},
{
'formula': 'formula 2',
'material': 'label 2',
'solvent': 'label 2',
'thickness': ' 2',
'thickness_enabled': 'False',
'thickness_enabled': false,
'roughness': '2',
'roughness_enabled': 'False',
'roughness_enabled': false,
'solvation': '2',
'apm': '2',
'apm_enabled': 'False',
},
{
'formula': 'formula 3',
'material': 'label 3',
'solvent': 'label 3',
'thickness': '3',
'thickness_enabled': 'False',
'thickness_enabled': false,
'roughness': '3',
'roughness_enabled': 'False',
'roughness_enabled': false,
'solvation': '3',
'apm': '3',
'apm_enabled': 'False',
},
]

Expand Down Expand Up @@ -306,9 +303,12 @@ QtObject {
// Layer magnetism (polarized analysis)
readonly property bool magnetismSupported: true
readonly property var layersMagnetism: [
{ 'label': 'label 1', 'magnetic': 'True', 'rho_m': '5.0', 'theta_m': '40.0', 'phi': '130.0', 'editable': 'True' },
{ 'label': 'label 2', 'magnetic': 'False', 'rho_m': '0.0', 'theta_m': '270.0', 'phi': '', 'editable': '' },
{ 'label': 'label 3', 'magnetic': 'False', 'rho_m': '0.0', 'theta_m': '270.0', 'phi': '', 'editable': '' },
{ 'label': 'label 1', 'magnetic': true, 'rho_m': '5.0', 'theta_m': '40.0', 'phi': '130.0', 'editable': true,
'material_magnetic': false, 'rho_m_coupled': false },
{ 'label': 'label 2', 'magnetic': false, 'rho_m': '0.0', 'theta_m': '270.0', 'phi': '', 'editable': false,
'material_magnetic': false, 'rho_m_coupled': false },
{ 'label': 'label 3', 'magnetic': false, 'rho_m': '0.0', 'theta_m': '270.0', 'phi': '', 'editable': false,
'material_magnetic': false, 'rho_m_coupled': false },
]
function setLayerMagneticAtIndex(index, value) {
console.debug(`setLayerMagneticAtIndex ${index} ${value}`)
Expand Down
26 changes: 8 additions & 18 deletions EasyReflectometryApp/Backends/Py/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from PySide6.QtCore import Signal
from PySide6.QtCore import Slot

from .logic.bayesian import VALID_INITIALIZERS
from .logic.bayesian import Bayesian as BayesianLogic
from .logic.calculators import Calculators as CalculatorsLogic
from .logic.experiment_selection import ExperimentSelection
Expand Down Expand Up @@ -276,7 +277,7 @@ def setBayesianInitializer(self, value: str) -> None:

@Property('QVariantList', notify=minimizerChanged)
def bayesianInitializerOptions(self) -> list:
return ['eps', 'cov', 'lhs', 'random']
return list(VALID_INITIALIZERS)

@Property(bool, notify=fittingChanged)
def bayesianResultAvailable(self) -> bool:
Expand Down Expand Up @@ -351,29 +352,17 @@ def _bayesian_display_name_list(self) -> list[str]:

@Property(str, notify=fittingChanged)
def bayesianCornerPlotUrl(self) -> str:
"""Return a file URL for the corner plot PNG, or empty string."""
if not self._bayesian_logic.has_result:
return ''
if not self._bayesian_logic.corner_plot_url:
self._render_corner_plot()
"""File URL of the corner plot HTML, or ''; rendered when sampling finishes, never on read."""
return self._bayesian_logic.corner_plot_url

@Property(str, notify=fittingChanged)
def bayesianTracePlotUrl(self) -> str:
"""Return a file URL for the trace plot PNG, or empty string."""
if not self._bayesian_logic.has_result:
return ''
if not self._bayesian_logic.trace_plot_url:
self._render_trace_plot()
"""File URL of the trace plot HTML, or ''; rendered when sampling finishes, never on read."""
return self._bayesian_logic.trace_plot_url

@Property(str, notify=fittingChanged)
def bayesianDistributionPlotUrl(self) -> str:
"""Return a file URL for the interactive distribution HTML, or empty string."""
if not self._bayesian_logic.has_result:
return ''
if not self._bayesian_logic.distribution_plot_url:
self._render_distribution_plot()
"""File URL of the distribution plot HTML, or ''; rendered when sampling finishes, never on read."""
return self._bayesian_logic.distribution_plot_url

@Property('QVariantMap', notify=fittingChanged)
Expand Down Expand Up @@ -659,6 +648,7 @@ def _on_sample_finished(self, results: list) -> None:
self._compute_diagnostics,
self._render_corner_plot,
self._render_trace_plot,
self._render_distribution_plot,
):
try:
step()
Expand Down Expand Up @@ -1384,7 +1374,7 @@ def modelParametersCount(self) -> int:
[
parameter
for parameter in self._parameters_logic.all_parameters()
if parameter.get('enabled', True) and not self._parameters_logic.is_experiment_parameter(parameter)
if parameter.get('enabled', True) and not parameter['experiment']
]
)

Expand All @@ -1394,7 +1384,7 @@ def experimentParametersCount(self) -> int:
[
parameter
for parameter in self._parameters_logic.all_parameters()
if parameter.get('enabled', True) and self._parameters_logic.is_experiment_parameter(parameter)
if parameter.get('enabled', True) and parameter['experiment']
]
)

Expand Down
31 changes: 9 additions & 22 deletions EasyReflectometryApp/Backends/Py/logic/assemblies.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def type_at_current_index(self) -> str:
return self._assemblies[self.index].type

@property
def assemblies(self) -> list[dict[str, str]]:
def assemblies(self) -> list[dict[str, str | list[str]]]:
rows = _from_assemblies_collection_to_list_of_dicts(self._assemblies)
# An assembly is one object in every model sharing it (contrasts): editing it edits all.
current = self._project_lib.current_model_index
Expand All @@ -76,15 +76,19 @@ def assemblies_names(self) -> list[str]:
def remove_at_index(self, index: int) -> None:
self._assemblies.remove_assembly(index)

def add_new(self) -> None:
def add_new(self) -> bool:
"""Insert an assembly with a Si layer; return whether the Si material had to be added to the project."""
previous_length = len(self._assemblies)
target_index = self._target_insert_index(self.index, previous_length)
existing_ids = {id(assembly) for assembly in self._assemblies}
self._assemblies.add_assembly()
new_index = self._move_new_assembly_into_position(existing_ids, target_index)
materials_before = len(self._project_lib._materials)
# The library adds Si to the project's materials when it is missing.
index_si = self._project_lib.get_index_si()
if new_index is not None:
self._assemblies[new_index].layers[0].material = self._project_lib._materials[index_si]
return len(self._project_lib._materials) > materials_before

def duplicate_selected(self) -> None:
if not self._has_valid_assembly_index(self.index):
Expand Down Expand Up @@ -176,23 +180,6 @@ def set_conformal_roughness(self, new_value: bool) -> bool:
return False


def _from_assemblies_collection_to_list_of_dicts(assemblies_collection: Sample) -> list[dict[str, str]]:
assemblies_list = []
for assembly in assemblies_collection:
assemblies_list.append(
{
'label': assembly.name,
'type': assembly.type,
'repetitions': 1,
'constrain_apm': 'False',
'conformal_roughness': 'False',
}
)
if isinstance(assembly, RepeatingMultilayer):
assemblies_list[-1]['repetitions'] = assembly.repetitions

if isinstance(assembly, SurfactantLayer):
assemblies_list[-1]['constrain_apm'] = assembly.constrain_area_per_molecule
assemblies_list[-1]['conformal_roughness'] = assembly.conformal_roughness

return assemblies_list
def _from_assemblies_collection_to_list_of_dicts(assemblies_collection: Sample) -> list[dict[str, str | list[str]]]:
# Repetitions and the surfactant flags have their own current-index properties.
return [{'label': assembly.name, 'type': assembly.type} for assembly in assemblies_collection]
6 changes: 3 additions & 3 deletions EasyReflectometryApp/Backends/Py/logic/bayesian.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""State container for Bayesian DREAM hyper-parameters and posterior results."""

_VALID_INITIALIZERS = ('eps', 'cov', 'lhs', 'random')
VALID_INITIALIZERS = ('eps', 'cov', 'lhs', 'random')


class Bayesian:
Expand Down Expand Up @@ -80,11 +80,11 @@ def initializer(self) -> str:

@initializer.setter
def initializer(self, value: str) -> None:
if value in _VALID_INITIALIZERS:
if value in VALID_INITIALIZERS:
self._initializer = value
else:
raise ValueError(
f'Unknown initializer {value!r}. Valid options: {_VALID_INITIALIZERS}'
f'Unknown initializer {value!r}. Valid options: {VALID_INITIALIZERS}'
)

# ------------------------------------------------------------------
Expand Down
57 changes: 28 additions & 29 deletions EasyReflectometryApp/Backends/Py/logic/layers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from easyreflectometry.sample import LayerAreaPerMolecule
from easyreflectometry.sample import LayerCollection
from easyreflectometry.sample import LayerMagnetism
from easyreflectometry.sample import Material
from easyreflectometry.sample import Sample
from easyreflectometry.sample.elements.layers.layer_magnetism import DEFAULTS as MAGNETISM_DEFAULTS

Expand Down Expand Up @@ -53,7 +52,7 @@ def name_at_current_index(self) -> str:
return self._layers[self.index].name

@property
def layers(self) -> list[dict[str, str]]:
def layers(self) -> list[dict[str, str | bool]]:
return _from_layers_collection_to_list_of_dicts(self._layers)

@property
Expand All @@ -66,14 +65,17 @@ def remove_at_index(self, index: int) -> None:
# model's field, acting on nothing; the project fixes those.
self._project_lib._sync_parameter_states()

def add_new(self) -> None:
if 'Si' not in [material.name for material in self._project_lib._materials]:
self._project_lib._materials.add_material(Material(name='Si', sld=2.07, isld=0.0))
index_si = [material.name for material in self._project_lib._materials].index('Si')
def add_new(self) -> bool:
"""Append a Si layer; return whether the Si material had to be added to the project."""
materials = self._project_lib._materials
count_before = len(materials)
# The library adds Si to the project's materials when it is missing.
si = materials[self._project_lib.get_index_si()]
self._layers.add_layer()
self._layers[-1].material = self._project_lib._materials[index_si]
self._layers[-1].material = si
# Set layer name based on material name
self._layers[-1].name = self._project_lib._materials[index_si].name + ' Layer'
self._layers[-1].name = si.name + ' Layer'
return len(materials) > count_before

def duplicate_selected(self) -> None:
self._layers.duplicate_layer(self.index)
Expand Down Expand Up @@ -217,33 +219,34 @@ def magnetism_supported(self) -> bool:
return bool(self._project_lib.calculator_supports_magnetism)

@property
def magnetism(self) -> list[dict[str, str]]:
def magnetism(self) -> list[dict[str, str | bool]]:
"""One row per layer of the current assembly, describing its magnetism.

``magnetic`` is 'True'/'False'; ``rho_m``/``theta_m`` carry the defaults
``magnetic`` is a bool; ``rho_m``/``theta_m`` carry the defaults
of a fresh :class:`LayerMagnetism` for non-magnetic layers so the fields
show what attaching magnetism would start from. ``phi`` is the direction
the moment points, in degrees from the guide field - what the compass
draws - and ``editable`` whether that direction can be set by dragging
it (a constrained ``theta_m`` follows its expression, not the pointer).
Both are empty for a non-magnetic layer, which has no direction.
``material_magnetic`` says whether the layer's material carries a
moment (a ``MaterialMagnetic``), and ``rho_m_coupled`` whether the
layer takes ``rho_m`` from it.
draws - and is empty for a non-magnetic layer, which has no direction.
``editable`` says whether that direction can be set by dragging it (a
constrained ``theta_m`` follows its expression, not the pointer); it is
False for a non-magnetic layer. ``material_magnetic`` says whether the
layer's material carries a moment (a ``MaterialMagnetic``), and
``rho_m_coupled`` whether the layer takes ``rho_m`` from it; both are
bools.
"""
rows = []
for layer in self._layers:
magnetism = getattr(layer, 'magnetism', None)
rows.append(
{
'label': layer.name,
'magnetic': str(magnetism is not None),
'magnetic': magnetism is not None,
'rho_m': str(magnetism.rho_m.value if magnetism is not None else _DEFAULT_RHO_M),
'theta_m': str(magnetism.theta_m.value if magnetism is not None else _DEFAULT_THETA_M),
'phi': '' if magnetism is None else str(magnetic_vector_for_layer(magnetism)['phi']),
'editable': '' if magnetism is None else str(magnetism.theta_m.independent),
'material_magnetic': str(getattr(layer.material, 'rho_m', None) is not None),
'rho_m_coupled': str(layer.rho_m_coupled),
'editable': magnetism is not None and bool(magnetism.theta_m.independent),
'material_magnetic': getattr(layer.material, 'rho_m', None) is not None,
'rho_m_coupled': bool(layer.rho_m_coupled),
}
)
return rows
Expand Down Expand Up @@ -381,15 +384,16 @@ def _set_parameter_value(parameter, new_value) -> bool:
return True


def _from_layers_collection_to_list_of_dicts(layers_collection: LayerCollection) -> list[dict[str, str]]:
def _from_layers_collection_to_list_of_dicts(layers_collection: LayerCollection) -> list[dict[str, str | bool]]:
"""Convert layers collection to list of dicts.

Editability of thickness and roughness mirrors each parameter's ``enabled``
flag, which ``Project._update_enablement_of_fixed_layers_for_model`` keeps
in step with the superphase (first layer of the first assembly) and the
subphase (last layer of the last assembly). Reading the flag rather than
re-deriving it from the assembly position keeps the table and the
parameters in agreement (issue #329).
parameters in agreement (issue #329). Only area-per-molecule layers carry
``formula``, ``apm``, ``solvent`` and ``solvation``.

:param layers_collection: The collection of layers.
"""
Expand All @@ -401,15 +405,10 @@ def _from_layers_collection_to_list_of_dicts(layers_collection: LayerCollection)
'roughness': str(layer.roughness.value),
'thickness': str(layer.thickness.value),
'material': layer.material.name,
'formula': 'formula',
'apm': '0.1',
'solvent': 'solvent',
'solvation': '0.2',
'apm_enabled': 'True',
# `enabled` is set on a parameter only once enablement has run
# over it; until then it counts as enabled, as in the library.
'thickness_enabled': str(bool(getattr(layer.thickness, 'enabled', True))),
'roughness_enabled': str(bool(getattr(layer.roughness, 'enabled', True))),
'thickness_enabled': bool(getattr(layer.thickness, 'enabled', True)),
'roughness_enabled': bool(getattr(layer.roughness, 'enabled', True)),
}
)
if isinstance(layer, LayerAreaPerMolecule):
Expand Down
Loading
Loading