@@ -118,12 +118,15 @@ def get_mesh(model_info, values, dim='1d', mono=False):
118118 active = lambda name : True
119119
120120 #print("in get_mesh: pars:",[p.id for p in parameters.call_parameters])
121- mesh = [_get_par_weights (p , values , active (p .name ))
121+ values = values .copy ()
122+ mesh = [_pop_par_weights (p , values , active (p .name ))
122123 for p in parameters .call_parameters ]
124+ if values :
125+ raise TypeError (f"Unused parameters in call: { ', ' .join (values .keys ())} " )
123126 return mesh
124127
125128
126- def _get_par_weights (parameter , values , active = True ):
129+ def _pop_par_weights (parameter , values , active = True ):
127130 # type: (Parameter, Dict[str, float], bool) -> Tuple[float, np.ndarray, np.ndarray]
128131 """
129132 Generate the distribution for parameter *name* given the parameter values
@@ -132,34 +135,27 @@ def _get_par_weights(parameter, values, active=True):
132135 Uses "name", "name_pd", "name_pd_type", "name_pd_n", "name_pd_sigma"
133136 from the *pars* dictionary for parameter value and parameter dispersion.
134137 """
135- value = float (values .get (parameter .name , parameter .default ))
136- npts = values .get (parameter .name + '_pd_n' , 0 )
137- width = values .get (parameter .name + '_pd' , 0.0 )
138- relative = parameter .relative_pd
139- if npts == 0 or width == 0.0 or not active :
140- # Note: orientation parameters have the viewing angle as the parameter
141- # value and the jitter in the distribution, so be sure to set the
142- # empty pd for orientation parameters to 0.
143- pd = [value if relative or not parameter .polydisperse else 0.0 ], [1.0 ]
138+ value = float (values .pop (parameter .name , parameter .default ))
139+ if parameter .polydisperse :
140+ npts = values .pop (parameter .name + '_pd_n' , 0 )
141+ width = values .pop (parameter .name + '_pd' , 0.0 )
142+ nsigma = values .pop (parameter .name + '_pd_nsigma' , 3.0 )
143+ distribution = values .pop (parameter .name + '_pd_type' , 'gaussian' )
144+ relative = parameter .relative_pd
145+ if npts == 0 or width == 0.0 or not active :
146+ # Note: orientation parameters have the viewing angle as the parameter
147+ # value and the jitter in the distribution, so be sure to set the
148+ # empty pd for orientation parameters to 0.
149+ pd = [value if relative else 0.0 ], [1.0 ]
150+ else :
151+ limits = parameter .limits
152+ pd = weights .get_weights (distribution , npts , width , nsigma ,
153+ value , limits , relative )
144154 else :
145- limits = parameter .limits
146- disperser = values .get (parameter .name + '_pd_type' , 'gaussian' )
147- nsigma = values .get (parameter .name + '_pd_nsigma' , 3.0 )
148- pd = weights .get_weights (disperser , npts , width , nsigma ,
149- value , limits , relative )
155+ pd = [value ], [1.0 ]
150156 return value , pd [0 ], pd [1 ]
151157
152158
153- def _vol_pars (model_info , values ):
154- # type: (ModelInfo, ParameterSet) -> Tuple[np.ndarray, np.ndarray]
155- vol_pars = [_get_par_weights (p , values )
156- for p in model_info .parameters .call_parameters
157- if p .type == 'volume' ]
158- #import pylab; pylab.plot(vol_pars[0][0],vol_pars[0][1]); pylab.show()
159- dispersity , weight = dispersion_mesh (model_info , vol_pars )
160- return dispersity , weight
161-
162-
163159def _make_sesans_transform (data ):
164160 # Pre-compute the Hankel matrix (H)
165161 SElength , SEunits = data .x , data ._xunit
@@ -260,10 +256,11 @@ def _interpret_data(self, data: Data, model: KernelModel) -> None:
260256 else :
261257 res = resolution .Perfect1D (q )
262258 elif (getattr (data , 'dxl' , None ) is not None
263- and getattr (data , 'dxw' , None ) is not None ):
264- res = resolution .Slit1D (data .x [index ],
265- qx_width = data .dxl [index ],
266- qy_width = data .dxw [index ])
259+ or getattr (data , 'dxw' , None ) is not None ):
260+ res = resolution .Slit1D (
261+ data .x [index ],
262+ q_length = None if data .dxl is None else data .dxl [index ],
263+ q_width = None if data .dxw is None else data .dxw [index ])
267264 else :
268265 res = resolution .Perfect1D (data .x [index ])
269266 elif self .data_type == 'Iq-oriented' :
@@ -278,9 +275,10 @@ def _interpret_data(self, data: Data, model: KernelModel) -> None:
278275 or getattr (data , 'dxw' , None ) is None ):
279276 raise ValueError ("oriented sample with 1D data needs slit resolution" )
280277
281- res = resolution2d .Slit2D (data .x [index ],
282- qx_width = data .dxw [index ],
283- qy_width = data .dxl [index ])
278+ res = resolution2d .Slit2D (
279+ data .x [index ],
280+ qx_width = data .dxw [index ],
281+ qy_width = data .dxl [index ])
284282 else :
285283 raise ValueError ("Unknown data type" ) # never gets here
286284
@@ -456,43 +454,142 @@ def test_reparameterize():
456454 except Exception :
457455 pass
458456
457+ def _direct_calculate (model , data , pars ):
458+ from .core import load_model_info , build_model
459+ model_info = load_model_info (model )
460+ kernel = build_model (model_info )
461+ calculator = DirectModel (data , kernel )
462+ return calculator (** pars )
463+
464+ def Iq (model , q , dq = None , ql = None , qw = None , ** pars ):
465+ """
466+ Compute I(q) for *model*. Resolution is *dq* for pinhole or *ql* and *qw*
467+ for slit geometry. Use 0 or None for infinite slits.
468+
469+ Model is the name of a builtin or custom model, or a model expression, such
470+ as sphere+sphere for a mixture of spheres of different radii, or
471+ sphere@hardsphere for concentrated solutions where the dilute approximation
472+ no longer applies.
473+
474+ Use additional keywords for model parameters, tagged with *_pd*, *_pd_n*,
475+ *_pd_nsigma*, *_pd_type* to set polydispersity parameters, or *_M0*,
476+ *_mphi*, *_mtheta* for magnetic parameters.
477+
478+ This is not intended for use when the same I(q) is evaluated many times
479+ with different parameter values. For that you should set up the model
480+ with `model = build_model(load_model_info(model_name))`, set up a data
481+ object to define q values and resolution, then use
482+ `calculator = DirectModel(data, model)` to set up a calculator, or
483+ `problem = bumps.FitProblem(sasmodels.bumps_model.Experiment(data, model))`
484+ to define a fit problem for uses with the bumps optimizer. Data can be
485+ loaded using the `sasdata` package, or use one of the empty data generators
486+ from `sasmodels.data`.
487+
488+ Models are cached. Custom models will not be reloaded even if the
489+ underlying files have changed. If you are using this in a long running
490+ application then you will need to call
491+ `sasmodels.direct_model._model_cache.clear()` to reset the cache and force
492+ custom model reload.
493+ """
494+ from .data import Data1D , _as_numpy
495+ data = Data1D (x = q , dx = dq )
496+ def broadcast (v ):
497+ return (
498+ None if v is None
499+ else np .full (len (q ), v ) if np .isscalar (v )
500+ else _as_numpy (v ))
501+ data .dxl , data .dxw = broadcast (ql ), broadcast (qw )
502+ return _direct_calculate (model , data , pars )
503+
504+ def Iqxy (model , qx , qy , dqx = None , dqy = None , ** pars ):
505+ """
506+ Compute I(qx, qy) for *model*. Resolution is *dqx* and *dqy*.
507+ See :func:`Iq` for details on model and parameters.
508+ """
509+ from .data import Data2D
510+ data = Data2D (x = qx , y = qy , dx = dqx , dy = dqy )
511+ return _direct_calculate (model , data , pars )
512+
513+ def Gxi (model , xi , ** pars ):
514+ """
515+ Compute SESANS correlation G' = G(xi) - G(0) for *model*.
516+ See :func:`Iq` for details on model and parameters.
517+ """
518+ from .data import empty_sesans
519+ data = empty_sesans (z = xi )
520+ return _direct_calculate (model , data , pars )
459521
460522def main ():
461523 # type: () -> None
462524 """
463525 Program to evaluate a particular model at a set of q values.
464526 """
465527 import sys
466- from .data import empty_data1D , empty_data2D
467- from .core import load_model_info , build_model
468528
469529 if len (sys .argv ) < 3 :
470530 print ("usage: python -m sasmodels.direct_model modelname (q|qx,qy) par=val ..." )
471531 sys .exit (1 )
472- model_name = sys .argv [1 ]
532+ model = sys .argv [1 ]
473533 call = sys .argv [2 ].upper ()
534+ pars = dict ((k , (float (v ) if not k .endswith ("_pd_type" ) else v ))
535+ for pair in sys .argv [3 :]
536+ for k , v in [pair .split ('=' )])
474537 try :
475538 values = [float (v ) for v in call .split (',' )]
476539 except ValueError :
477540 values = []
478541 if len (values ) == 1 :
479542 q , = values
480- data = empty_data1D ([q ])
543+ dq = dqw = dql = None
544+ #dq = [q*0.05] # 5% pinhole resolution
545+ #dqw, dql = [q*0.05], [1.0] # 5% horizontal slit resolution
546+ print (Iq (model , [q ], dq = dq , qw = dqw , ql = dql , ** pars )[0 ])
547+ #print(Gxi(model, [q], **pars)[0])
481548 elif len (values ) == 2 :
482549 qx , qy = values
483- data = empty_data2D ([qx ], [qy ])
550+ dq = None
551+ #dq = [0.005] # 5% pinhole resolution at q = 0.1
552+ print (Iqxy (model , [qx ], [qy ], dqx = dq , dqy = dq , ** pars )[0 ])
484553 else :
485554 print ("use q or qx,qy" )
486555 sys .exit (1 )
487556
488- model_info = load_model_info (model_name )
489- model = build_model (model_info )
490- calculator = DirectModel (data , model )
491- pars = dict ((k , (float (v ) if not k .endswith ("_pd_type" ) else v ))
492- for pair in sys .argv [3 :]
493- for k , v in [pair .split ('=' )])
494- Iq = calculator (** pars )
495- print (Iq [0 ])
557+ def test_simple_interface ():
558+ def near (value , target ):
559+ """Close enough in single precision"""
560+ #print(f"value: {value}, target: {target}")
561+ return np .allclose (value , target , rtol = 1e-6 , atol = 0 , equal_nan = True )
562+ # Note: target values taken from running main() on parameters.
563+ # Resolution was 5% dq/q.
564+ pars = dict (radius = 200 )
565+ # simple sphere in 1D (perfect, pinhole, slit)
566+ assert near (Iq ('sphere' , [0.1 ], ** pars ), [0.6200146273894904 ])
567+ assert near (Iq ('sphere' , [0.1 ], dq = [0.005 ], ** pars ), [2.3019224683980215 ])
568+ assert near (Iq ('sphere' , [0.1 ], qw = [0.005 ], ql = [1.0 ], ** pars ), [0.3673431784535172 ])
569+ # simple sphere in 2D (perfect, pinhole)
570+ assert near (Iqxy ('sphere' , [0.1 ], [0.1 ], ** pars ), [1.1781532874802199 ])
571+ assert near (Iqxy ('sphere' , [0.1 ], [0.1 ], dqx = [0.005 ], dqy = [0.005 ], ** pars ),
572+ [0.8177780778578667 ])
573+ # sesans
574+ assert near (Gxi ('sphere' , [100 ], ** pars ), [- 0.19146959126623486 ])
575+ # Check that single point sesans matches value in an array
576+ xi = np .logspace (1 , 3 , 100 )
577+ y = Gxi ('sphere' , xi , ** pars )
578+ for k in (0 , len (xi )// 5 , len (xi )// 2 , len (xi )- 1 ):
579+ ysingle = Gxi ('sphere' , [xi [k ]], ** pars )[0 ]
580+ print (f"SESANS point check { k } : xi={ xi [k ]:.1f} single={ ysingle :.4f} vector={ y [k ]:.4f} " )
581+ assert abs ((ysingle - y [k ])/ y [k ]) < 0.1 , "SESANS point value not matching vector value within 10%"
582+ # magnetic 2D
583+ pars = dict (radius = 200 , sld_M0 = 3 , sld_mtheta = 30 )
584+ assert near (Iqxy ('sphere' , [0.1 ], [0.1 ], ** pars ), [1.5577852226925908 ])
585+ # polydisperse 1D
586+ pars = dict (
587+ radius = 200 , radius_pd = 0.1 , radius_pd_n = 15 , radius_pd_nsigma = 2.5 ,
588+ radius_pd_type = "uniform" )
589+ assert near (Iq ('sphere' , [0.1 ], ** pars ), [2.703169824954617 ])
496590
497591if __name__ == "__main__" :
592+ import logging
593+ logging .disable (logging .ERROR )
498594 main ()
595+ #test_simple_interface()
0 commit comments