Skip to content

Commit 08e45a3

Browse files
committed
Update plots to support ProbeSets, fix bug with in-place updating of Rq
1 parent ab47f67 commit 08e45a3

1 file changed

Lines changed: 132 additions & 70 deletions

File tree

  • molgroups/refl1d_interface

molgroups/refl1d_interface/sas.py

Lines changed: 132 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from bumps.parameter import Parameter
1010
from bumps.webview.server.custom_plot import CustomWebviewPlot
1111
from refl1d.experiment import Experiment
12+
from refl1d.probe import ProbeSet
1213
from refl1d.probe.resolution import dTdL2dQ, sigma2FWHM
1314
from refl1d.webview.server.colors import COLORS
1415

@@ -87,8 +88,20 @@ def sas(self):
8788
""" Calculate the small angle scattering I(q) """
8889
key = ("small_angle_scattering")
8990
if key not in self._cache:
90-
probes = [self.probe] if not hasattr(self.probe, 'probes') else self.probe.probes
91-
Iq = np.hstack([self._calc_Iq(probe, dtheta_l) for probe, dtheta_l in zip(probes, self.sas_model.dtheta_l)])
91+
probes = [self.probe] if not isinstance(self.probe, ProbeSet) else self.probe.probes
92+
93+
# Broadcast dtheta_l if it is None or a scalar
94+
dtheta_val = self.sas_model.dtheta_l
95+
if np.isscalar(dtheta_val) or dtheta_val is None:
96+
dtheta_list = [dtheta_val] * len(probes)
97+
else:
98+
dtheta_list = dtheta_val
99+
100+
# Calculate and Stack
101+
Iq_parts = [self._calc_Iq(probe, dt) for probe, dt in zip(probes, dtheta_list)]
102+
self._cache[key] = np.hstack(Iq_parts)
103+
104+
Iq = np.hstack([self._calc_Iq(probe, dtheta_l) for probe, dtheta_l in zip(probes, dtheta_list)])
92105
self._cache[key] = Iq
93106
return self._cache[key]
94107

@@ -98,7 +111,7 @@ def reflectivity(self, resolution=True, interpolation=0):
98111

99112
# 2. Add SAS signal
100113
if self.sas_model is not None:
101-
Rq += self.sas()
114+
Rq = Rq + self.sas()
102115
return Q, Rq
103116

104117
# --- 2. CONCRETE CLASSES ---
@@ -140,74 +153,113 @@ def sas_decomposition_plot(model: SASReflectivityExperiment, problem=None) -> Cu
140153
Webview plot that shows the decomposition of the signal into
141154
Reflectivity (Rq) and SAS (Iq) components.
142155
143-
Args:
144-
model: The SASReflectivityExperiment instance (passed by webview)
145-
problem: The Bumps FitProblem (passed by webview)
156+
Supports both single Probe and ProbeSet.
146157
"""
147158

148-
# 2. Calculate Components
149-
# Total Theory = R(q) + I(q)
150-
# We call reflectivity() which returns the sum
151-
Q, total_theory = model.reflectivity()
159+
# 1. Helpers for Flattening
160+
def to_flat(arr):
161+
if arr is None: return np.array([])
162+
return np.ravel(np.array(arr, dtype=float))
163+
164+
# 2. Get Concatenated Theory Components
165+
# model.reflectivity() and sas() return 1D arrays matching the full concatenated Q
166+
Q_all_raw, total_theory_raw = model.reflectivity()
167+
Q_all = to_flat(Q_all_raw)
168+
total_theory = to_flat(total_theory_raw)
152169

153-
# SANS Component = I(q)
154-
# We call sas() directly. Handle case where sas_model might be None
155170
if model.sas_model is not None:
156-
Iq = model.sas()
171+
Iq_all = to_flat(model.sas())
157172
else:
158-
Iq = np.zeros_like(Q)
173+
Iq_all = np.zeros_like(Q_all)
159174

160-
# Reflectivity Component = R(q)
161-
# Derived by subtraction to ensure consistency
162-
Rq = total_theory - Iq
175+
Rq_all = total_theory - Iq_all
163176

164-
# 3. Get Data for comparison
165-
data_y = model.probe.R
166-
data_dy = model.probe.dR
177+
# 3. Identify Probes (Single vs ProbeSet)
178+
if hasattr(model.probe, 'probes'):
179+
probes = model.probe.probes
180+
else:
181+
probes = [model.probe]
167182

168183
# 4. Construct Plotly Figure
169184
fig = go.Figure()
185+
186+
# Cursor to track where we are in the concatenated arrays
187+
cursor = 0
188+
189+
# Loop over probes to slice data and plot traces
190+
for i, probe in enumerate(probes):
191+
# Determine slice range for this probe
192+
n_points = len(probe.Q)
193+
start = cursor
194+
end = cursor + n_points
195+
196+
# Slice the arrays
197+
Q = Q_all[start:end]
198+
Total = total_theory[start:end]
199+
Rq = Rq_all[start:end]
200+
Iq = Iq_all[start:end]
201+
202+
# Get Data for this probe
203+
data_y = to_flat(probe.R)
204+
data_dy = to_flat(probe.dR)
205+
206+
# Define Color for this Probe (Cycle through COLORS)
207+
base_color = COLORS[i % len(COLORS)]
208+
209+
# -- Trace: Data --
210+
fig.add_trace(go.Scatter(
211+
x=Q, y=data_y,
212+
error_y=dict(
213+
type='data',
214+
array=data_dy,
215+
visible=True,
216+
color=base_color,
217+
thickness=1
218+
),
219+
mode='markers',
220+
name=f'Data (Probe {i+1})',
221+
marker=dict(
222+
color=base_color,
223+
symbol='circle',
224+
size=6,
225+
opacity=0.4
226+
),
227+
legendgroup=f'group{i}'
228+
))
170229

171-
# -- Trace: Data --
172-
fig.add_trace(go.Scatter(
173-
x=Q, y=data_y,
174-
error_y=dict(
175-
type='data',
176-
array=data_dy,
177-
visible=True,
178-
color='rgba(0, 0, 0, 0.25)' # <--- Explicit opacity for error bars
179-
),
180-
mode='markers',
181-
name='Data',
182-
marker=dict(
183-
color='rgba(0, 0, 0, 0.25)', # <--- Explicit opacity for markers (0.25 = 25% visible)
184-
size=6
185-
)
186-
))
187-
188-
# -- Trace: Total Theory --
189-
fig.add_trace(go.Scatter(
190-
x=Q, y=total_theory,
191-
mode='lines',
192-
name='Total Model (R+I)',
193-
line=dict(color=COLORS[0], width=3)
194-
))
195-
196-
# -- Trace: Reflectivity Component --
197-
fig.add_trace(go.Scatter(
198-
x=Q, y=Rq,
199-
mode='lines',
200-
name='Reflectivity R(q)',
201-
line=dict(color=COLORS[1], width=2, dash='dash')
202-
))
203-
204-
# -- Trace: SANS Component --
205-
fig.add_trace(go.Scatter(
206-
x=Q, y=Iq,
207-
mode='lines',
208-
name='SANS I(q)',
209-
line=dict(color=COLORS[2], width=2, dash='dot')
210-
))
230+
# -- Trace: Total Theory (Solid) --
231+
fig.add_trace(go.Scatter(
232+
x=Q, y=Total,
233+
mode='lines',
234+
name=f'Total (Probe {i+1})',
235+
line=dict(color=base_color, width=3),
236+
legendgroup=f'group{i}'
237+
))
238+
239+
# -- Trace: Reflectivity (Dash) --
240+
# showlegend=True explicitly added
241+
fig.add_trace(go.Scatter(
242+
x=Q, y=Rq,
243+
mode='lines',
244+
name=f'Refl (Probe {i+1})',
245+
line=dict(color=base_color, width=2, dash='dash'),
246+
legendgroup=f'group{i}',
247+
showlegend=True
248+
))
249+
250+
# -- Trace: SANS (Dot) --
251+
# showlegend=True explicitly added
252+
fig.add_trace(go.Scatter(
253+
x=Q, y=Iq,
254+
mode='lines',
255+
name=f'SANS (Probe {i+1})',
256+
line=dict(color=base_color, width=2, dash='dot'),
257+
legendgroup=f'group{i}',
258+
showlegend=True
259+
))
260+
261+
# Advance cursor
262+
cursor += n_points
211263

212264
# 5. Styling
213265
fig.update_layout(
@@ -216,24 +268,34 @@ def sas_decomposition_plot(model: SASReflectivityExperiment, problem=None) -> Cu
216268
xaxis_type='linear',
217269
template='plotly_white',
218270
yaxis=dict(
219-
title='Intensity (R + I)',
220-
type='log',
221-
exponentformat='power', # <--- This forces 10^x notation
222-
showexponent='all' # Ensures exponents are shown for all ticks
223-
),
271+
title='Intensity (R + I)',
272+
type='log',
273+
exponentformat='power',
274+
showexponent='all'
275+
),
224276
legend=dict(x=0.01, y=0.01, xanchor='left', yanchor='bottom', bgcolor='rgba(255,255,255,0.8)')
225277
)
226278

227-
# 6. Prepare CSV Export Data
228-
# Simple CSV format: Q, Data, Error, Total, Rq, Iq
279+
# 6. Prepare CSV Export Data (Concatenated)
229280
csv_header = "Q,R,dR,Theory,Rq,Iq\n"
230281
csv_rows = []
231-
for i in range(len(Q)):
232-
row = f"{float(Q[i]):.6e},{float(data_y[i]):.6e},{float(data_dy[i]):.6e},{float(total_theory[i]):.6e},{float(Rq[i]):.6e},{float(Iq[i]):.6e}"
282+
283+
n_pts_total = min(len(Q_all), len(total_theory))
284+
285+
# Re-flatten probe data for CSV export
286+
if hasattr(model.probe, 'probes'):
287+
all_data_y = np.hstack([to_flat(p.R) for p in model.probe.probes])
288+
all_data_dy = np.hstack([to_flat(p.dR) for p in model.probe.probes])
289+
else:
290+
all_data_y = to_flat(model.probe.R)
291+
all_data_dy = to_flat(model.probe.dR)
292+
293+
for i in range(n_pts_total):
294+
row = f"{Q_all[i]:.6e},{all_data_y[i]:.6e},{all_data_dy[i]:.6e},{total_theory[i]:.6e},{Rq_all[i]:.6e},{Iq_all[i]:.6e}"
233295
csv_rows.append(row)
234296

235297
export_data = csv_header + "\n".join(csv_rows)
236298

237299
return CustomWebviewPlot(fig_type='plotly',
238300
plotdata=fig,
239-
exportdata=export_data)
301+
exportdata=export_data)

0 commit comments

Comments
 (0)