Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
0de292f
state reset. changed wording on empty project
rozyczko Mar 27, 2026
44eca00
remove dependency on EasyApp footer. Implement locally
rozyczko Mar 27, 2026
67b8fc5
fixed log display for experiment/analysis
rozyczko Mar 29, 2026
5427db7
wording on some elements improved
rozyczko Mar 30, 2026
3d85f9d
update the plot colors in the Analysis chart as well.
rozyczko Mar 30, 2026
996cd82
experiment lines should be dotted
rozyczko Mar 30, 2026
f4098fe
small fix to display proper experiment colors on non-full selections
rozyczko Mar 30, 2026
b0e60e5
make the dotted charts slightly more visible
rozyczko Mar 30, 2026
1202e21
errors should have grey foreground, not being editable
rozyczko Apr 7, 2026
8dc2671
use proper symbol for legend markers
rozyczko Apr 8, 2026
b088050
Win signing (#301)
rozyczko Apr 9, 2026
05b5137
replace "backspace" icon with "undo" for axes reset
rozyczko Apr 13, 2026
10a026b
initial implementation for lmfit
rozyczko Apr 14, 2026
fe4b8a4
added counter display in status bar
rozyczko Apr 14, 2026
413ddd5
added DFO display - "Fitting running ..." with increasing dot number
rozyczko Apr 15, 2026
2b0fd50
Combine layer and model editors; fix issues in lists (#303)
rozyczko Apr 20, 2026
eba3621
Merge branch 'intermediate_updates' into 1.2.0_fixes2
rozyczko Apr 22, 2026
3543504
properly scaled residuals display
rozyczko Apr 22, 2026
c58319e
zero display for residuals, initialization errors fixed.
rozyczko Apr 23, 2026
dff6cc2
replaced `undo` icon with `home` for chart axis reset
rozyczko Apr 27, 2026
b4ee744
react to changed resolution % in the exp tab
rozyczko Apr 27, 2026
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
43 changes: 16 additions & 27 deletions .github/workflows/installer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,37 +114,26 @@ jobs:
# ${{ secrets.APPLE_CERT_DATA }} ${{ secrets.APPLE_CERT_PASSWORD }}
# ${{ secrets.APPLE_NOTARY_USER }} ${{ secrets.APPLE_NOTARY_PASSWORD }}

- name: Install DigiCert Client tools from Github Custom Actions marketplace
if: |
runner.os == 'windows' && github.event_name == 'push'
uses: digicert/ssm-code-signing@v1.0.1

- name: Set up P12 certificate
if: |
runner.os == 'windows' && github.event_name == 'push'
- name: Setup SM_CLIENT_CERT_FILE from base64 secret data
if: runner.os == 'Windows'
run: |
echo "${{ secrets.WINDOWS_CERT_DATA }}" | base64 --decode > /d/Certificate_pkcs12.p12
echo "${{ secrets.KEYLOCKER_CERT_DATA }}" | base64 --decode > /d/Certificate_pkcs12.p12
shell: bash

- name: Set keylocker variables
if: |
runner.os == 'windows' && github.event_name == 'push'
id: variables
run: |
echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
echo "SM_HOST=${{ secrets.KEYLOCKER_HOST }}" >> "$GITHUB_ENV"
echo "SM_API_KEY=${{ secrets.KEYLOCKER_API_KEY }}" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_FILE=D:\\Certificate_pkcs12.p12" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.WINDOWS_CERT_PASSWORD }}" >> "$GITHUB_ENV"
shell: bash

- name: Sign the binary using keypair alias
if: |
runner.os == 'windows' && github.event_name == 'push' && env.BRANCH_NAME == 'master'
run: |
smctl sign --keypair-alias key_911959544 --input ${{ env.SETUP_EXE_PATH }}
shell: cmd

- name: Setup Software Trust Manager
if: runner.os == 'Windows'
uses: digicert/code-signing-software-trust-action@v1
with:
simple-signing-mode: true
# If the below 2 parameters are supplied, then smctl executable is invoked to attempt the signing.
input: ${{ env.SETUP_EXE_PATH }}
keypair-alias: ${{ secrets.KEYLOCKER_KEYPAIR_ALIAS }}
env:
SM_HOST: ${{ secrets.KEYLOCKER_HOST }}
SM_API_KEY: ${{ secrets.KEYLOCKER_API_KEY }}
SM_CLIENT_CERT_FILE: D:\\Certificate_pkcs12.p12
SM_CLIENT_CERT_PASSWORD: ${{ secrets.WINDOWS_CERT_PASSWORD }}

- name: Create zip archive of offline app installer for distribution
run: >
Expand Down
7 changes: 7 additions & 0 deletions EasyReflectometryApp/Backends/Mock/Analysis.qml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ QtObject {
readonly property string fitErrorMessage: ''
readonly property int fitNumRefinedParams: 3
readonly property real fitChi2: 1.2345
readonly property int fitIteration: 0
readonly property real fitInterimChi2: 0.0
readonly property real fitInterimReducedChi2: 0.0
readonly property string fitProgressMessage: ''
readonly property bool fitHasInterimUpdate: false
readonly property bool fitHasPreviewUpdate: false
readonly property var fitPreviewParameterValues: ({})
readonly property var fitResults: ({ success: true, nvarys: 3, chi2: 1.2345 })

// Fit failure signal (mirrors Python backend)
Expand Down
45 changes: 41 additions & 4 deletions EasyReflectometryApp/Backends/Py/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,34 @@ def fitNumRefinedParams(self) -> int:
def fitChi2(self) -> float:
return self._fitting_logic.fit_chi2

@Property(int, notify=fittingChanged)
def fitIteration(self) -> int:
return self._fitting_logic.fit_iteration

@Property(float, notify=fittingChanged)
def fitInterimChi2(self) -> float:
return self._fitting_logic.fit_interim_chi2

@Property(float, notify=fittingChanged)
def fitInterimReducedChi2(self) -> float:
return self._fitting_logic.fit_interim_reduced_chi2

@Property(str, notify=fittingChanged)
def fitProgressMessage(self) -> str:
return self._fitting_logic.fit_progress_message

@Property(bool, notify=fittingChanged)
def fitHasInterimUpdate(self) -> bool:
return self._fitting_logic.fit_has_interim_update

@Property(bool, notify=fittingChanged)
def fitHasPreviewUpdate(self) -> bool:
return self._fitting_logic.fit_has_preview_update

@Property('QVariant', notify=fittingChanged)
def fitPreviewParameterValues(self) -> dict:
return self._fitting_logic.fit_preview_parameter_values

@Property('QVariant', notify=fittingChanged)
def fitResults(self) -> dict:
"""Return fit results as a dict for QML consumption."""
Expand Down Expand Up @@ -171,10 +199,17 @@ def _start_threaded_fit(self) -> None:
self._fitter_thread.setTerminationEnabled(True)
self._fitter_thread.finished.connect(self._on_fit_finished)
self._fitter_thread.failed.connect(self._on_fit_failed)
self._fitter_thread.progressDetail.connect(self._on_fit_progress)
self._fitter_thread.finished.connect(self._fitter_thread.deleteLater)
self._fitter_thread.failed.connect(self._fitter_thread.deleteLater)
self._fitter_thread.start()

@Slot(dict)
def _on_fit_progress(self, payload: dict) -> None:
"""Handle in-flight progress payloads emitted from the worker thread."""
self._fitting_logic.on_fit_progress(payload)
self.fittingChanged.emit()

@Slot(list)
def _on_fit_finished(self, results: list) -> None:
"""Handle successful completion of threaded fit."""
Expand All @@ -187,21 +222,23 @@ def _on_fit_finished(self, results: list) -> None:
@Slot(str)
def _on_fit_failed(self, error_message: str) -> None:
"""Handle failed threaded fit."""
is_user_cancel = self._fitting_logic.fit_cancelled and 'cancel' in error_message.lower()
if is_user_cancel:
error_message = 'Fitting cancelled by user'
self._fitting_logic.on_fit_failed(error_message)
self._fitter_thread = None
self.fittingChanged.emit()
self._clearCacheAndEmitParametersChanged()
self.externalFittingChanged.emit()
self.fitFailed.emit(error_message)
if not is_user_cancel:
self.fitFailed.emit(error_message)

@Slot()
def _onStopFit(self) -> None:
"""Stop fitting and clean up."""
self._fitting_logic.stop_fit()
if self._fitter_thread is not None:
self._fitter_thread.stop()
self._fitter_thread.deleteLater()
self._fitter_thread = None
self.fittingChanged.emit()
self.externalFittingChanged.emit()

Expand Down Expand Up @@ -455,7 +492,7 @@ def get_individual_experiment_data_list(self):
if exp_idx < len(self._experiments_logic.available())
else f'Experiment {exp_idx + 1}'
)
color = color_palette[idx % len(color_palette)]
color = color_palette[exp_idx % len(color_palette)]

experiment_data_list.append({'data': data, 'name': exp_name, 'color': color, 'index': exp_idx})
except (IndexError, AttributeError) as e:
Expand Down
6 changes: 6 additions & 0 deletions EasyReflectometryApp/Backends/Py/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ def setBackground(self, new_value: float) -> None:
self.experimentChanged.emit()
self.externalExperimentChanged.emit()

@Slot(str)
def setResolution(self, new_value: str) -> None:
if self._model_logic.set_resolution_at_current_index(new_value):
self.experimentChanged.emit()
self.externalExperimentChanged.emit()

# Actions
@Slot(str)
def load(self, paths: str) -> None:
Expand Down
66 changes: 57 additions & 9 deletions EasyReflectometryApp/Backends/Py/logic/assemblies.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,29 @@ class Assemblies:
def __init__(self, project_lib: ProjectLib):
self._project_lib = project_lib

def _has_valid_assembly_index(self, index: int) -> bool:
return 0 <= index < len(self._assemblies)

def _target_insert_index(self, current_index: int, previous_length: int) -> int:
if previous_length <= 1:
return previous_length
return min(current_index + 1, previous_length - 1)

def _move_new_assembly_into_position(self, existing_ids: set[int], target_index: int) -> int | None:
new_index = next((idx for idx, assembly in enumerate(self._assemblies) if id(assembly) not in existing_ids), None)
if new_index is None:
return None

while new_index > target_index:
self._assemblies.move_up(new_index)
new_index -= 1

while new_index < target_index:
self._assemblies.move_down(new_index)
new_index += 1

return new_index

@property
def _assemblies(self) -> Sample:
return self._project_lib._models[self._project_lib.current_model_index].sample # Sample is a collection of assemblies
Expand Down Expand Up @@ -43,12 +66,23 @@ def remove_at_index(self, value: str) -> None:
self._assemblies.remove_assembly(int(value))

def add_new(self) -> None:
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)
index_si = self._project_lib.get_index_si()
self._assemblies[-1].layers[0].material = self._project_lib._materials[index_si]
if new_index is not None:
self._assemblies[new_index].layers[0].material = self._project_lib._materials[index_si]

def duplicate_selected(self) -> None:
if not self._has_valid_assembly_index(self.index):
return
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.duplicate_assembly(self.index)
self._move_new_assembly_into_position(existing_ids, target_index)

def move_selected_up(self) -> None:
if self.index > 0:
Expand All @@ -60,31 +94,45 @@ def move_selected_down(self) -> None:
self._assemblies.move_down(self.index)
self.index = self.index + 1

def set_name_at_current_index(self, new_value: str) -> None:
self._assemblies[self.index].name = new_value
return True
def set_name_at_current_index(self, new_value: str) -> bool:
return self.set_name_at_index(self.index, new_value)

def set_name_at_index(self, index: int, new_value: str) -> bool:
if not self._has_valid_assembly_index(index):
return False
if self._assemblies[index].name != new_value:
self._assemblies[index].name = new_value
return True
return False

def set_type_at_current_index(self, new_value: str) -> bool:
if new_value == self._assemblies[self.index].type:
return self.set_type_at_index(self.index, new_value)

def set_type_at_index(self, index: int, new_value: str) -> bool:
if not self._has_valid_assembly_index(index):
return False
if new_value == self._assemblies[index].type:
return False

if new_value == 'Multi-layer':
new_assembly = Multilayer()
new_assembly.layers[0].material = self._assemblies[self.index].layers.data[0].material
new_assembly.layers[0].material = self._assemblies[index].layers.data[0].material
elif new_value == 'Repeating Multi-layer':
new_assembly = RepeatingMultilayer(repetitions=1, name=new_value)
new_assembly.layers[0].material = self._assemblies[self.index].layers.data[0].material
new_assembly.layers[0].material = self._assemblies[index].layers.data[0].material
elif new_value == 'Surfactant Layer':
index_air = self._project_lib.get_index_air()
index_d2o = self._project_lib.get_index_d2o()
new_assembly = SurfactantLayer()
new_assembly.layers[0].solvent = self._project_lib._materials[index_air]
new_assembly.layers[1].solvent = self._project_lib._materials[index_d2o]
else:
return False

if new_assembly.name is None:
new_assembly.name = self._assemblies[self.index].name
new_assembly.name = self._assemblies[index].name

self._assemblies[self.index] = new_assembly
self._assemblies[index] = new_assembly
return True

# Only for repeating multilayer
Expand Down
Loading
Loading