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
6 changes: 6 additions & 0 deletions examples/AdvancedPy/src/AdvancedPy.pyproject
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
"AdvancedPy/Gui/Globals/References.qml",
"AdvancedPy/Gui/Pages/Home/Content.qml",
"AdvancedPy/Gui/Pages/Home/Popups/About.qml",
"AdvancedPy/Gui/Pages/Analysis/Layout.qml",
"AdvancedPy/Gui/Pages/Analysis/MainArea/Chart.qml",
"AdvancedPy/Gui/Pages/Analysis/Sidebar/Basic/Layout.qml",
"AdvancedPy/Gui/Pages/Analysis/Sidebar/Basic/Groups/GenerateData.qml",
"AdvancedPy/Gui/Pages/Project/Layout.qml",
"AdvancedPy/Gui/Pages/Project/MainArea/Description.qml",
"AdvancedPy/Gui/Pages/Project/Sidebar/Basic/Layout.qml",
Expand All @@ -31,12 +35,14 @@
"AdvancedPy/Gui/Pages/Report/Sidebar/Extra/Groups/Empty.qml",
"AdvancedPy/Backends/qmldir",
"AdvancedPy/Backends/MockBackend.qml",
"AdvancedPy/Backends/MockQml/Analysis.qml",
"AdvancedPy/Backends/MockQml/Project.qml",
"AdvancedPy/Backends/MockQml/Report.qml",
"AdvancedPy/Backends/MockQml/Status.qml",
"AdvancedPy/Backends/MockQml/qmldir",
"AdvancedPy/Backends/real_backend.py",
"AdvancedPy/Backends/real_py/project.py",
"AdvancedPy/Backends/real_py/analysis.py",
"AdvancedPy/Backends/real_py/report.py",
"AdvancedPy/Backends/real_py/status.py",
"AdvancedPy/Backends/real_py/logic/helpers.py"
Expand Down
3 changes: 1 addition & 2 deletions examples/AdvancedPy/src/AdvancedPy/Backends/MockBackend.qml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ import Backends.MockQml as MockLogic
QtObject {

property var project: MockLogic.Project
property var analysis: MockLogic.Analysis
property var status: MockLogic.Status
property var report: MockLogic.Report

}


48 changes: 48 additions & 0 deletions examples/AdvancedPy/src/AdvancedPy/Backends/MockQml/Analysis.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: 2024 EasyApp contributors
// SPDX-License-Identifier: BSD-3-Clause
// © 2024 Contributors to the EasyApp project <https://github.com/easyscience/EasyApp>

pragma Singleton

import QtQuick
import QtGraphs

import Gui.Globals as Globals


QtObject {

property int dataSize: 50
property var axesRanges: {
"xmin": 0.0,
"xmax": 180.0,
"ymin": 0.0,
"ymax": 100.0,
}

signal dataPointsChanged(var points)

function generateData() {
console.debug(`* Generating ${dataSize} data points...`)
const xmin = axesRanges.xmin
const xmax = axesRanges.xmax
const ymin = axesRanges.ymin
const ymax = axesRanges.ymax

const pointCount = Math.max(1, dataSize)
const stepSize = pointCount > 1 ? (xmax - xmin) / (pointCount - 1) : 0

let dataPoints = []
for (let i = 0; i < pointCount; i++) {
const x = xmin + i * stepSize
const y = ymin + Math.random() * (ymax - ymin)
dataPoints.push(Qt.point(x, y))
}
console.debug(" Data generation completed.")

console.debug(`* Sending ${pointCount} data points to series...`)
dataPointsChanged(dataPoints)
console.debug(" Data update signal emitted.")
}

}
1 change: 1 addition & 0 deletions examples/AdvancedPy/src/AdvancedPy/Backends/MockQml/qmldir
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module MockQml

singleton Project Project.qml
singleton Analysis Analysis.qml
singleton Report Report.qml
singleton Status Status.qml
6 changes: 6 additions & 0 deletions examples/AdvancedPy/src/AdvancedPy/Backends/real_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from EasyApp.Logic.Logging import LoggerLevelHandler

from .real_py.project import Project
from .real_py.analysis import Analysis
from .real_py.status import Status
from .real_py.report import Report

Expand All @@ -21,6 +22,7 @@ def __init__(self):

# Individual Backend objects
self._project = Project()
self._analysis = Analysis()
self._status = Status()
self._report = Report()

Expand All @@ -47,6 +49,10 @@ def __init__(self):
def project(self):
return self._project

@Property('QVariant', constant=True)
def analysis(self):
return self._analysis

@Property('QVariant', constant=True)
def status(self):
return self._status
Expand Down
132 changes: 132 additions & 0 deletions examples/AdvancedPy/src/AdvancedPy/Backends/real_py/analysis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# SPDX-FileCopyrightText: 2024 EasyApp contributors
# SPDX-License-Identifier: BSD-3-Clause
# © 2024 Contributors to the EasyApp project <https://github.com/easyscience/EasyApp>

import numpy as np
from PySide6.QtCore import QObject, Signal, Slot, Property, QPointF

from EasyApp.Logic.Logging import console


class Analysis(QObject):
"""
Backend object that generates synthetic diffraction-like data
and exposes it to QML as a list of QPointF values plus axis ranges.
"""

# Signals
dataSizeChanged = Signal()
dataPointsChanged = Signal("QVariantList") # Emitted with list<QPointF>
axesRangesChanged = Signal() # Emitted when range dict updates

def __init__(self):
super().__init__()

self._dataSize = 10000
self._axesRanges = {
"xmin": 0.0,
"xmax": 180.0,
"ymin": 0.0,
"ymax": 100.0,
}

# ------------------------------------------------------------------
# QML-accessible Properties
# ------------------------------------------------------------------

@Property(int, notify=dataSizeChanged)
def dataSize(self):
"""Number of X/Y data points to generate."""
return self._dataSize

@dataSize.setter
def dataSize(self, value):
value = int(value)
if self._dataSize == value:
return
self._dataSize = value
self.dataSizeChanged.emit()

@Property("QVariantMap", notify=axesRangesChanged)
def axesRanges(self):
"""
Axis ranges used by the graph:
{ "xmin": float, "xmax": float, "ymin": float, "ymax": float }
Access in QML using: axisX.min: analysis.axesRanges["xmin"]
"""
return self._axesRanges

# ------------------------------------------------------------------
# Public Slot Called from QML
# ------------------------------------------------------------------

@Slot()
def generateData(self):
"""Generate new synthetic data and notify QML."""
console.debug(f"* Generating {self.dataSize} data points...")
x, y = self._generate_data(n_points=self.dataSize)
console.debug(" Data generation completed.")

console.debug(f"* Converting and sending {self.dataSize} data points to series...")
self.dataPointsChanged.emit(self._ndarrays_to_qpoints(x, y))
console.debug(" Data update signal emitted.")

self._updateAxesRanges(x.min(), x.max(), y.min(), y.max())

# ------------------------------------------------------------------
# Internal Helpers
# ------------------------------------------------------------------

def _updateAxesRanges(self, xmin, xmax, ymin, ymax):
"""Store axis ranges and notify QML."""
vmargin = 10.0
self._axesRanges["xmin"] = float(xmin)
self._axesRanges["xmax"] = float(xmax)
self._axesRanges["ymin"] = max(0, float(ymin) - vmargin)
self._axesRanges["ymax"] = float(ymax) + vmargin
self.axesRangesChanged.emit()

@staticmethod
def _ndarrays_to_qpoints(x: np.ndarray, y: np.ndarray):
"""
Convert NumPy X/Y arrays to list[QPointF].
Uses memoryview to avoid Python float conversions inside numpy.
"""
mvx = memoryview(x)
mvy = memoryview(y)
return [QPointF(xi, yi) for xi, yi in zip(mvx, mvy)]

@staticmethod
def _generate_data(
n_points=2000,
n_peaks=100,
x_range=(0.0, 180.0),
intensity_range=(0, 100),
width_range=(0.05, 0.5),
noise_level=1.0,
background=20.0,
):
"""
Generate synthetic diffraction-like pattern from sum of random Gaussians.
Returns (x, y) NumPy arrays.
"""
# Sample x grid
x = np.linspace(*x_range, n_points)
y = np.zeros_like(x)

# Random peak positions, intensities, widths
positions = np.random.uniform(*x_range, n_peaks)
amplitudes = np.random.uniform(*intensity_range, n_peaks)
widths = np.random.uniform(*width_range, n_peaks)

# Gaussian peak contributions
for pos, amp, width in zip(positions, amplitudes, widths):
y += amp * np.exp(-0.5 * ((x - pos) / width) ** 2)

# Noise
y += np.random.normal(scale=noise_level, size=n_points)

# Background
y += background

return x, y
14 changes: 14 additions & 0 deletions examples/AdvancedPy/src/AdvancedPy/Gui/ApplicationWindow.qml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,19 @@ EaComponents.ApplicationWindow {
},
// Project page

// Analysis page
EaElements.AppBarTabButton {
id: analysisButton
enabled: false
fontIcon: 'microscope'
text: qsTr('Analysis')
ToolTip.text: qsTr('Calculation and fitting page')
Component.onCompleted: {
Globals.References.applicationWindow.appBarCentralTabs.analysisButton = analysisButton
}
},
// Analysis page

// Summary page
EaElements.AppBarTabButton {
id: summaryButton
Expand All @@ -93,6 +106,7 @@ EaComponents.ApplicationWindow {
contentArea: [
Loader { source: 'Pages/Home/Content.qml' },
Loader { source: 'Pages/Project/Layout.qml' },
Loader { source: 'Pages/Analysis/Layout.qml' },
Loader { source: 'Pages/Report/Layout.qml' }
]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ pragma Singleton
import QtQuick

// This module is registered in the main.py file and allows access to the properties
// and backend methods of the singleton object of the ‘PyBackend’ class.
// and backend methods of the singleton object of the ‘PyBackend’ class.
// If ‘PyBackend’ is not defined, then 'MockBackend' from directory 'Backends' is used.
// It is needed to run the GUI frontend via the qml runtime tool without any Python backend.
import Backends as Backends
Expand Down Expand Up @@ -56,6 +56,24 @@ QtObject {
function projectSave() { activeBackend.project.save() }
function projectEditInfo(path, new_value) { activeBackend.project.editInfo(path, new_value) }

////////////////
// Analysis page
////////////////

// All properties and methods related to the analysis page, unlike other pages,
// are accessed directly via the `activeBackend` object, without an intermediate
// wrapper in this file.
//
// They are defined in the following files:
// - Backends/MockQml/Analysis.qml
// - Backends/real_py/analysis.py
//
// This approach is used to reduce duplication of objects between the backend
// and frontend, and to minimize the number of automatically generated signals/slots.
//
// TODO: Profile performance and memory usage, and decide whether to use this
// direct access approach or keep a wrapper layer.

///////////////
// Summary page
///////////////
Expand Down
15 changes: 15 additions & 0 deletions examples/AdvancedPy/src/AdvancedPy/Gui/Globals/References.qml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ QtObject {
'appBarCentralTabs': {
'homeButton': null,
'projectButton': null,
'analysisButton': null,
'summaryButton': null,
}
}
Expand All @@ -31,6 +32,20 @@ QtObject {
}
}
}
},
'analysis': {
'sidebar': {
'basic': {
'slider': null
}
},
'mainarea': {
'description': {
'graph': {
'lineseries': null
}
}
}
}
}

Expand Down
49 changes: 49 additions & 0 deletions examples/AdvancedPy/src/AdvancedPy/Gui/Pages/Analysis/Layout.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-FileCopyrightText: 2024 EasyApp contributors
// SPDX-License-Identifier: BSD-3-Clause
// © 2024 Contributors to the EasyApp project <https://github.com/easyscience/EasyApp>

import QtQuick
import QtQuick.Controls

import EasyApp.Gui.Style as EaStyle
import EasyApp.Gui.Globals as EaGlobals
import EasyApp.Gui.Elements as EaElements
import EasyApp.Gui.Components as EaComponents

import Gui.Globals as Globals


EaComponents.ContentPage {

mainView: EaComponents.MainContent {
tabs: [
EaElements.TabButton { text: qsTr('Chart') }
]

items: [
Loader { source: 'MainArea/Chart.qml' }
]
}

sideBar: EaComponents.SideBar {
tabs: [
EaElements.TabButton { text: qsTr('Basic controls') }
]

items: [
Loader { source: 'Sidebar/Basic/Layout.qml' }
]

continueButton.text: qsTr('Continue')

continueButton.onClicked: {
console.debug(`Clicking '${continueButton.text}' button ::: ${this}`)
Globals.References.applicationWindow.appBarCentralTabs.summaryButton.enabled = true
Globals.References.applicationWindow.appBarCentralTabs.summaryButton.toggle()
}
}

Component.onCompleted: console.debug(`Analysis page loaded ::: ${this}`)
Component.onDestruction: console.debug(`Analysis page destroyed ::: ${this}`)

}
Loading