Changeset 9087214 in sasview for src/sas/sascalc
- Timestamp:
- Oct 11, 2016 11:09:47 AM (8 years ago)
- Branches:
- master, ESS_GUI, ESS_GUI_Docs, ESS_GUI_batch_fitting, ESS_GUI_bumps_abstraction, ESS_GUI_iss1116, ESS_GUI_iss879, ESS_GUI_iss959, ESS_GUI_opencl, ESS_GUI_ordering, ESS_GUI_sync_sascalc, costrafo411, magnetic_scatt, release-4.1.1, release-4.1.2, release-4.2.2, ticket-1009, ticket-1094-headless, ticket-1242-2d-resolution, ticket-1243, ticket-1249, ticket885, unittest-saveload
- Children:
- 4581ac9, 7949dcf7
- Parents:
- 392056d (diff), 46dfee9 (diff)
Note: this is a merge changeset, the changes displayed below correspond to the merge itself.
Use the (diff) links above to see all the changes relative to each parent. - Location:
- src/sas/sascalc
- Files:
-
- 6 edited
Legend:
- Unmodified
- Added
- Removed
-
src/sas/sascalc/fit/MultiplicationModel.py
rcb4ef58 r68669da 8 8 r""" 9 9 Use for P(Q)\*S(Q); function call must be in the order of P(Q) and then S(Q): 10 The model parameters are combined from both models, P(Q) and S(Q), except 1) ' effect_radius' of S(Q)11 which will be calculated from P(Q) via calculate_ER(), 12 and 2) 'scale' in P model which is synchronized w/ volfraction in S 10 The model parameters are combined from both models, P(Q) and S(Q), except 1) 'radius_effective' of S(Q) 11 which will be calculated from P(Q) via calculate_ER(), 12 and 2) 'scale' in P model which is synchronized w/ volfraction in S 13 13 then P*S is multiplied by a new parameter, 'scale_factor'. 14 14 The polydispersion is applicable only to P(Q), not to S(Q). … … 34 34 ## Parameter details [units, min, max] 35 35 self.details = {} 36 37 ##models 36 37 ## Define parameters to exclude from multiplication model 38 self.excluded_params={'radius_effective','scale','background'} 39 40 ##models 38 41 self.p_model = p_model 39 self.s_model = s_model 42 self.s_model = s_model 40 43 self.magnetic_params = [] 41 44 ## dispersion … … 45 48 ## New parameter:Scaling factor 46 49 self.params['scale_factor'] = 1 47 50 self.params['background'] = 0 51 48 52 ## Parameter details [units, min, max] 49 53 self._set_details() 50 54 self.details['scale_factor'] = ['', 0.0, numpy.inf] 51 55 self.details['background'] = ['',-numpy.inf,numpy.inf] 56 52 57 #list of parameter that can be fitted 53 self._set_fixed_params() 58 self._set_fixed_params() 54 59 ## parameters with orientation 55 60 for item in self.p_model.orientation_params: 56 61 self.orientation_params.append(item) 57 for item in self.p_model.magnetic_params: 58 self.magnetic_params.append(item) 62 for item in self.p_model.magnetic_params: 63 self.magnetic_params.append(item) 59 64 for item in self.s_model.orientation_params: 60 65 if not item in self.orientation_params: … … 66 71 multiplicity = 1 67 72 ## functional multiplicity of the model 68 self.multiplicity = multiplicity 69 73 self.multiplicity = multiplicity 74 70 75 # non-fittable parameters 71 self.non_fittable = p_model.non_fittable 72 self.multiplicity_info = [] 76 self.non_fittable = p_model.non_fittable 77 self.multiplicity_info = [] 73 78 self.fun_list = {} 74 79 if self.non_fittable > 1: 75 80 try: 76 self.multiplicity_info = p_model.multiplicity_info 81 self.multiplicity_info = p_model.multiplicity_info 77 82 self.fun_list = p_model.fun_list 78 83 self.is_multiplicity_model = True … … 82 87 self.is_multiplicity_model = False 83 88 self.multiplicity_info = [0] 84 89 85 90 def _clone(self, obj): 86 91 """ … … 96 101 #obj = copy.deepcopy(self) 97 102 return obj 98 99 103 104 100 105 def _set_dispersion(self): 101 106 """ … … 103 108 applied to s_model 104 109 """ 105 ##set dispersion only from p_model 110 ##set dispersion only from p_model 106 111 for name , value in self.p_model.dispersion.iteritems(): 107 self.dispersion[name] = value 108 112 self.dispersion[name] = value 113 109 114 def getProfile(self): 110 115 """ 111 116 Get SLD profile of p_model if exists 112 117 113 118 :return: (r, beta) where r is a list of radius of the transition points\ 114 119 beta is a list of the corresponding SLD values … … 121 126 x = None 122 127 y = None 123 128 124 129 return x, y 125 130 126 131 def _set_params(self): 127 132 """ 128 133 Concatenate the parameters of the two models to create 129 these model parameters 134 these model parameters 130 135 """ 131 136 132 137 for name , value in self.p_model.params.iteritems(): 133 if not name in self.params.keys() and name != 'scale':138 if not name in self.params.keys() and name not in self.excluded_params: 134 139 self.params[name] = value 135 140 136 141 for name , value in self.s_model.params.iteritems(): 137 #Remove the effect_radiusfrom the (P*S) model parameters.138 if not name in self.params.keys() and name != 'effect_radius':142 #Remove the radius_effective from the (P*S) model parameters. 143 if not name in self.params.keys() and name not in self.excluded_params: 139 144 self.params[name] = value 140 145 141 146 # Set "scale and effec_radius to P and S model as initializing 142 147 # since run P*S comes from P and S separately. 148 self._set_backgrounds() 143 149 self._set_scale_factor() 144 self._set_ effect_radius()145 150 self._set_radius_effective() 151 146 152 def _set_details(self): 147 153 """ 148 154 Concatenate details of the two models to create 149 this model's details 155 this model's details 150 156 """ 151 157 for name, detail in self.p_model.details.iteritems(): 152 if name != 'scale':158 if name not in self.excluded_params: 153 159 self.details[name] = detail 154 160 155 161 for name , detail in self.s_model.details.iteritems(): 156 if not name in self.details.keys() or name != 'effect_radius':162 if not name in self.details.keys() or name not in self.exluded_params: 157 163 self.details[name] = detail 158 164 165 def _set_backgrounds(self): 166 """ 167 Set component backgrounds to zero 168 """ 169 if 'background' in self.p_model.params: 170 self.p_model.setParam('background',0) 171 if 'background' in self.s_model.params: 172 self.s_model.setParam('background',0) 173 174 159 175 def _set_scale_factor(self): 160 176 """ … … 162 178 """ 163 179 value = self.params['volfraction'] 164 if value != None: 180 if value != None: 165 181 factor = self.p_model.calculate_VR() 166 182 if factor == None or factor == NotImplemented or factor == 0.0: … … 170 186 self.p_model.setParam('scale', value) 171 187 self.s_model.setParam('volfraction', val) 172 173 def _set_ effect_radius(self):188 189 def _set_radius_effective(self): 174 190 """ 175 191 Set effective radius to S(Q) model 176 192 """ 177 if not ' effect_radius' in self.s_model.params.keys():193 if not 'radius_effective' in self.s_model.params.keys(): 178 194 return 179 195 effective_radius = self.p_model.calculate_ER() 180 196 #Reset the effective_radius of s_model just before the run 181 197 if effective_radius != None and effective_radius != NotImplemented: 182 self.s_model.setParam(' effect_radius', effective_radius)183 198 self.s_model.setParam('radius_effective', effective_radius) 199 184 200 def setParam(self, name, value): 185 """ 201 """ 186 202 Set the value of a model parameter 187 203 188 204 :param name: name of the parameter 189 205 :param value: value of the parameter … … 191 207 # set param to P*S model 192 208 self._setParamHelper( name, value) 193 194 ## setParam to p model 195 # set 'scale' in P(Q) equal to volfraction 209 210 ## setParam to p model 211 # set 'scale' in P(Q) equal to volfraction 196 212 if name == 'volfraction': 197 213 self._set_scale_factor() 198 elif name in self.p_model.getParamList() :214 elif name in self.p_model.getParamList() and name not in self.excluded_params: 199 215 self.p_model.setParam( name, value) 200 201 ## setParam to s model 202 # This is a little bit abundant: Todo: find better way 203 self._set_ effect_radius()204 if name in self.s_model.getParamList() :216 217 ## setParam to s model 218 # This is a little bit abundant: Todo: find better way 219 self._set_radius_effective() 220 if name in self.s_model.getParamList() and name not in self.excluded_params: 205 221 if name != 'volfraction': 206 222 self.s_model.setParam( name, value) 207 223 208 224 209 225 #self._setParamHelper( name, value) 210 226 211 227 def _setParamHelper(self, name, value): 212 228 """ … … 228 244 self.params[item] = value 229 245 return 230 246 231 247 raise ValueError, "Model does not contain parameter %s" % name 232 233 248 249 234 250 def _set_fixed_params(self): 235 251 """ … … 240 256 241 257 self.fixed.sort() 242 243 258 259 244 260 def run(self, x = 0.0): 245 """ 261 """ 246 262 Evaluate the model 247 263 248 264 :param x: input q-value (float or [float, float] as [r, theta]) 249 265 :return: (scattering function value) 250 266 """ 251 267 # set effective radius and scaling factor before run 252 self._set_ effect_radius()268 self._set_radius_effective() 253 269 self._set_scale_factor() 254 270 return self.params['scale_factor'] * self.p_model.run(x) * \ 255 self.s_model.run(x) 271 self.s_model.run(x) + self.params['background'] 256 272 257 273 def runXY(self, x = 0.0): 258 """ 274 """ 259 275 Evaluate the model 260 276 261 277 :param x: input q-value (float or [float, float] as [qx, qy]) 262 278 :return: scattering function value 263 """ 279 """ 264 280 # set effective radius and scaling factor before run 265 self._set_ effect_radius()281 self._set_radius_effective() 266 282 self._set_scale_factor() 267 283 out = self.params['scale_factor'] * self.p_model.runXY(x) * \ 268 self.s_model.runXY(x) 284 self.s_model.runXY(x) + self.params['background'] 269 285 return out 270 271 ## Now (May27,10) directly uses the model eval function 286 287 ## Now (May27,10) directly uses the model eval function 272 288 ## instead of the for-loop in Base Component. 273 289 def evalDistribution(self, x = []): 274 """ 290 """ 275 291 Evaluate the model in cartesian coordinates 276 292 277 293 :param x: input q[], or [qx[], qy[]] 278 294 :return: scattering function P(q[]) 279 295 """ 280 296 # set effective radius and scaling factor before run 281 self._set_ effect_radius()297 self._set_radius_effective() 282 298 self._set_scale_factor() 283 299 out = self.params['scale_factor'] * self.p_model.evalDistribution(x) * \ 284 self.s_model.evalDistribution(x) 300 self.s_model.evalDistribution(x) + self.params['background'] 285 301 return out 286 302 … … 288 304 """ 289 305 Set the dispersion object for a model parameter 290 306 291 307 :param parameter: name of the parameter [string] 292 308 :dispersion: dispersion object of type DispersionModel … … 299 315 return value 300 316 except: 301 raise 317 raise 302 318 303 319 def fill_description(self, p_model, s_model): … … 306 322 """ 307 323 description = "" 308 description += "Note:1) The effect_radius(effective radius) of %s \n"%\324 description += "Note:1) The radius_effective (effective radius) of %s \n"%\ 309 325 (s_model.name) 310 326 description += " is automatically calculated " … … 318 334 description += " for details of individual models." 319 335 self.description += description 320 -
src/sas/sascalc/data_util/qsmearing.py
rf8aa738 r392056d 5 5 #This software was developed by the University of Tennessee as part of the 6 6 #Distributed Data Analysis of Neutron Scattering Experiments (DANSE) 7 #project funded by the US National Science Foundation. 7 #project funded by the US National Science Foundation. 8 8 #See the license text in license.txt 9 9 #copyright 2008, University of Tennessee … … 13 13 import logging 14 14 import sys 15 from sasmodels import sesans 15 16 16 from sasmodels.resolution import Slit1D, Pinhole1D 17 from sasmodels.resolution import Slit1D, Pinhole1D, SESANS1D 17 18 from sasmodels.resolution2d import Pinhole2D 18 19 … … 48 49 49 50 # Look for resolution smearing data 51 _found_sesans = False 52 if data.dx is not None and data.meta_data['loader']=='SESANS': 53 if data.dx[0] > 0.0: 54 _found_sesans = True 55 56 if _found_sesans == True: 57 return sesans_smear(data, model) 58 50 59 _found_resolution = False 51 60 if data.dx is not None and len(data.dx) == len(data.x): … … 92 101 self.model = model 93 102 self.resolution = resolution 94 self.offset = numpy.searchsorted(self.resolution.q_calc, self.resolution.q[0]) 103 if hasattr(self.resolution, 'data'): 104 if self.resolution.data.meta_data['loader'] == 'SESANS': 105 self.offset = 0 106 # This is default behaviour, for future resolution/transform functions this needs to be revisited. 107 else: 108 self.offset = numpy.searchsorted(self.resolution.q_calc, self.resolution.q[0]) 109 else: 110 self.offset = numpy.searchsorted(self.resolution.q_calc, self.resolution.q[0]) 111 112 #self.offset = numpy.searchsorted(self.resolution.q_calc, self.resolution.q[0]) 95 113 96 114 def apply(self, iq_in, first_bin=0, last_bin=None): … … 126 144 q[first:last+1]. 127 145 """ 146 128 147 q = self.resolution.q 129 148 first = numpy.searchsorted(q, q_min) … … 142 161 width = data.dx if data.dx is not None else 0 143 162 return PySmear(Pinhole1D(q, width), model) 163 164 def sesans_smear(data, model=None): 165 #This should be calculated characteristic length scale 166 #Probably not a data prameter either 167 #Need function to calculate this based on model 168 #Here assume a number 169 Rmax = 1000000 170 q_calc = sesans.make_q(data.sample.zacceptance, Rmax) 171 return PySmear(SESANS1D(data,q_calc),model) -
src/sas/sascalc/dataloader/data_info.py
r1b1a1c1 r1fac6c0 25 25 import numpy 26 26 import math 27 28 class plottable_sesans1D(object):29 """30 SESANS is a place holder for 1D SESANS plottables.31 32 #TODO: This was directly copied from the plottables_1D. Modified Somewhat.33 #Class has been updated.34 """35 # The presence of these should be mutually36 # exclusive with the presence of Qdev (dx)37 x = None38 y = None39 lam = None40 dx = None41 dy = None42 dlam = None43 ## Slit smearing length44 dxl = None45 ## Slit smearing width46 dxw = None47 48 # Units49 _xaxis = ''50 _xunit = ''51 _yaxis = ''52 _yunit = ''53 54 def __init__(self, x, y, lam, dx=None, dy=None, dlam=None):55 # print "SESANS plottable working"56 self.x = numpy.asarray(x)57 self.y = numpy.asarray(y)58 self.lam = numpy.asarray(lam)59 if dx is not None:60 self.dx = numpy.asarray(dx)61 if dy is not None:62 self.dy = numpy.asarray(dy)63 if dlam is not None:64 self.dlam = numpy.asarray(dlam)65 66 def xaxis(self, label, unit):67 """68 set the x axis label and unit69 """70 self._xaxis = label71 self._xunit = unit72 73 def yaxis(self, label, unit):74 """75 set the y axis label and unit76 """77 self._yaxis = label78 self._yunit = unit79 80 81 27 class plottable_1D(object): 82 28 """ … … 94 40 dxw = None 95 41 42 ## SESANS specific params (wavelengths for spin echo length calculation) 43 44 lam = None 45 dlam = None 46 96 47 # Units 97 48 _xaxis = '' … … 100 51 _yunit = '' 101 52 102 def __init__(self, x, y, dx=None, dy=None, dxl=None, dxw=None ):53 def __init__(self, x, y, dx=None, dy=None, dxl=None, dxw=None, lam=None, dlam=None): 103 54 self.x = numpy.asarray(x) 104 55 self.y = numpy.asarray(y) … … 111 62 if dxw is not None: 112 63 self.dxw = numpy.asarray(dxw) 64 if lam is not None: 65 self.lam = numpy.asarray(lam) 66 if dlam is not None: 67 self.dlam = numpy.asarray(dlam) 113 68 114 69 def xaxis(self, label, unit): … … 736 691 return self._perform_union(other) 737 692 738 class SESANSData1D(plottable_sesans1D, DataInfo): 739 """ 740 SESANS 1D data class 741 """ 742 x_unit = 'nm' 743 y_unit = 'pol' 744 745 def __init__(self, x=None, y=None, lam=None, dx=None, dy=None, dlam=None): 693 class Data1D(plottable_1D, DataInfo): 694 """ 695 1D data class 696 """ 697 #if plottable_1D.lam is None: # This means it's SANS data! 698 # x_unit = '1/A' 699 # y_unit = '1/cm' 700 #elif plottable_1D.lam is not None: # This means it's SESANS data! 701 # x_unit = 'A' 702 # y_unit = 'pol' 703 #else: # and if it's neither, you get punished! 704 # raise(TypeError,'This is neither SANS nor SESANS data, what the hell are you doing??') 705 706 def __init__(self, x=None, y=None, dx=None, dy=None, lam=None, dlam=None, isSesans=False): 707 self.isSesans = isSesans 746 708 DataInfo.__init__(self) 747 plottable_sesans1D.__init__(self, x, y, lam, dx, dy, dlam) 709 plottable_1D.__init__(self, x, y, dx, dy,None, None, lam, dlam) 710 if self.isSesans: 711 x_unit = 'A' 712 y_unit = 'pol' 713 elif not self.isSesans: # it's SANS data! (Could also be simple else statement, but i prefer exhaustive conditionals...-JHB) 714 x_unit = '1/A' 715 y_unit = '1/cm' 716 else: # and if it's neither, you get punished! 717 raise(TypeError,'This is neither SANS nor SESANS data, what the hell are you doing??') 748 718 749 719 def __str__(self): … … 759 729 return _str 760 730 761 def clone_without_data(self, length=0, clone=None):762 """763 Clone the current object, without copying the data (which764 will be filled out by a subsequent operation).765 The data arrays will be initialized to zero.766 767 :param length: length of the data array to be initialized768 :param clone: if provided, the data will be copied to clone769 """770 from copy import deepcopy771 if clone is None or not issubclass(clone.__class__, Data1D):772 x = numpy.zeros(length)773 dx = numpy.zeros(length)774 y = numpy.zeros(length)775 dy = numpy.zeros(length)776 clone = Data1D(x, y, dx=dx, dy=dy)777 778 clone.title = self.title779 clone.run = self.run780 clone.filename = self.filename781 clone.instrument = self.instrument782 clone.notes = deepcopy(self.notes)783 clone.process = deepcopy(self.process)784 clone.detector = deepcopy(self.detector)785 clone.sample = deepcopy(self.sample)786 clone.source = deepcopy(self.source)787 clone.collimation = deepcopy(self.collimation)788 clone.trans_spectrum = deepcopy(self.trans_spectrum)789 clone.meta_data = deepcopy(self.meta_data)790 clone.errors = deepcopy(self.errors)791 792 return clone793 794 class Data1D(plottable_1D, DataInfo):795 """796 1D data class797 """798 x_unit = '1/A'799 y_unit = '1/cm'800 801 def __init__(self, x, y, dx=None, dy=None):802 DataInfo.__init__(self)803 plottable_1D.__init__(self, x, y, dx, dy)804 805 def __str__(self):806 """807 Nice printout808 """809 _str = "%s\n" % DataInfo.__str__(self)810 _str += "Data:\n"811 _str += " Type: %s\n" % self.__class__.__name__812 _str += " X-axis: %s\t[%s]\n" % (self._xaxis, self._xunit)813 _str += " Y-axis: %s\t[%s]\n" % (self._yaxis, self._yunit)814 _str += " Length: %g\n" % len(self.x)815 return _str816 817 731 def is_slit_smeared(self): 818 732 """ … … 843 757 y = numpy.zeros(length) 844 758 dy = numpy.zeros(length) 845 clone = Data1D(x, y, dx=dx, dy=dy) 759 lam = numpy.zeros(length) 760 dlam = numpy.zeros(length) 761 clone = Data1D(x, y, lam=lam, dx=dx, dy=dy, dlam=dlam ) 846 762 847 763 clone.title = self.title -
src/sas/sascalc/dataloader/readers/sesans_reader.py
r1c0e3b0 r392056d 8 8 import numpy 9 9 import os 10 from sas.sascalc.dataloader.data_info import SESANSData1D10 from sas.sascalc.dataloader.data_info import Data1D 11 11 12 12 # Check whether we have a converter available … … 84 84 tdx = numpy.zeros(0) 85 85 # print "all good" 86 output = SESANSData1D(x=x, y=y, lam=lam, dy=dy, dx=dx, dlam=dlam)86 output = Data1D(x=x, y=y, lam=lam, dy=dy, dx=dx, dlam=dlam, isSesans=True ) 87 87 # print output 88 88 self.filename = output.filename = basename … … 121 121 paramvals.append(toks[1]) 122 122 if len(toks)>5: 123 #zvals.append(toks[0]) 124 #dzvals.append(toks[1]) 125 #lamvals.append(toks[2]) 126 #dlamvals.append(toks[3]) 127 #Pvals.append(toks[4]) 128 #dPvals.append(toks[5]) 129 123 130 zvals.append(toks[0]) 124 dzvals.append(toks[ 1])125 lamvals.append(toks[ 2])126 dlamvals.append(toks[ 3])127 Pvals.append(toks[ 4])128 dPvals.append(toks[ 5])131 dzvals.append(toks[3]) 132 lamvals.append(toks[4]) 133 dlamvals.append(toks[5]) 134 Pvals.append(toks[1]) 135 dPvals.append(toks[2]) 129 136 else: 130 137 continue … … 140 147 default_z_unit = "A" 141 148 data_conv_P = None 142 default_p_unit = " " 149 default_p_unit = " " # Adjust unit for axis (L^-3) 143 150 lam_unit = lam_header[1].replace("[","").replace("]","") 151 if lam_unit == 'AA': 152 lam_unit = 'A' 144 153 varheader=[zvals[0],dzvals[0],lamvals[0],dlamvals[0],Pvals[0],dPvals[0]] 145 154 valrange=range(1, len(zvals)) … … 161 170 output.x, output.x_unit = self._unit_conversion(x, lam_unit, default_z_unit) 162 171 output.y = y 172 output.y_unit = '\AA^{-2} cm^{-1}' # output y_unit erbij 163 173 output.dx, output.dx_unit = self._unit_conversion(dx, lam_unit, default_z_unit) 164 174 output.dy = dy … … 166 176 output.dlam, output.dlam_unit = self._unit_conversion(dlam, lam_unit, default_z_unit) 167 177 168 output.xaxis("\ rm{z}", output.x_unit)169 output.yaxis("\\rm{ P/P0}", output.y_unit)178 output.xaxis("\\rm{z}", output.x_unit) 179 output.yaxis("\\rm{ln(P)/(t \lambda^2)}", output.y_unit) # Adjust label to ln P/(lam^2 t), remove lam column refs 170 180 # Store loading process information 171 181 output.meta_data['loader'] = self.type_name 172 output.sample.thickness = float(paramvals[6])182 #output.sample.thickness = float(paramvals[6]) 173 183 output.sample.name = paramvals[1] 174 184 output.sample.ID = paramvals[0] 175 185 zaccept_unit_split = paramnames[7].split("[") 176 186 zaccept_unit = zaccept_unit_split[1].replace("]","") 177 if zaccept_unit.strip() == '\AA^-1' :187 if zaccept_unit.strip() == '\AA^-1' or zaccept_unit.strip() == '\A^-1': 178 188 zaccept_unit = "1/A" 179 189 output.sample.zacceptance=(float(paramvals[7]),zaccept_unit) -
src/sas/sascalc/fit/AbstractFitEngine.py
rfc18690 r7988501 131 131 a way to get residuals from data. 132 132 """ 133 def __init__(self, x, y, dx=None, dy=None, smearer=None, data=None ):133 def __init__(self, x, y, dx=None, dy=None, smearer=None, data=None, lam=None, dlam=None): 134 134 """ 135 135 :param smearer: is an object of class QSmearer or SlitSmearer … … 152 152 153 153 """ 154 Data1D.__init__(self, x=x, y=y, dx=dx, dy=dy )154 Data1D.__init__(self, x=x, y=y, dx=dx, dy=dy, lam=lam,dlam=dlam) 155 155 self.num_points = len(x) 156 156 self.sas_data = data -
src/sas/sascalc/fit/BumpsFitting.py
rb699768 r7988501 26 26 from bumps import parameter 27 27 from bumps.fitproblem import FitProblem 28 29 28 30 29 from sas.sascalc.fit.AbstractFitEngine import FitEngine
Note: See TracChangeset
for help on using the changeset viewer.