99from bumps .parameter import Parameter
1010from bumps .webview .server .custom_plot import CustomWebviewPlot
1111from refl1d .experiment import Experiment
12+ from refl1d .probe import ProbeSet
1213from refl1d .probe .resolution import dTdL2dQ , sigma2FWHM
1314from 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