Skip to content

Commit

Permalink
Lots of model-as-dict code and tests; unfinished
Browse files Browse the repository at this point in the history
  • Loading branch information
perwin committed Sep 2, 2021
1 parent 897eaab commit 8d8ec1e
Show file tree
Hide file tree
Showing 9 changed files with 283 additions and 55 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@
changes may contain significant changes to the API.


## 0.11.0 -- 2021-09-xx
## Added
Models (including current parameter values) can now be described by a dict-based format.

Pre-compiled version for Python 3.9 on macOS.

### Changed
The interface to the FunctionSetDescription class has changed: the "name" parameter is
now optional (and defaults to None).


## 0.10.0 -- 2020-11-20
### Changed
Updated to use version 1.8 of Imfit, including new image functions (GaussianRingAz, FlatBar)
Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 40 additions & 0 deletions new_notes_and_todo_for_python-wrapper_code.txt
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,46 @@ PLAN:
[x] Update code till tests pass
[x] Update code-with-parameter-limits till tests pass

[x] Code to generate parameter-info list from Parameter object
--- getParamInfoList
returns e.g. [33.4] or [33.4, 0.0, 100.0] or [33.4, 'fixed']
[x] Add test to test_descriptions.py for ParameterDescription.getParamInfoList()
[x] Write code till test passes

[x] Code to generate function-as-dict from FunctionDescription object
--- getFunctionAsDict
[x] Add test to test_descriptions.py for FunctionDescription.getFunctionAsDict()
[x] Write code till test passes

[x] Code to generate function-set-as-dict from FunctionSetDescription object
--- getFuncSetAsDict
[x] Add test to test_descriptions.py for FunctionSetDescription.getFuncSetAsDict()
[ ] Write code till test passes

[x] Code to generate model-as-dict from ModelDescription object
--- getModelAsDict
[x] Add test to test_descriptions.py for ModelDescription.getModelAsDict()
[x] Write code till test passes

[ ] Convert "name" to "label" in FunctionSetDescription code

[ ] Clean up config.parse_config() so that it *doesn't* auto-generate "fs{0}" (e.g., "fs0")
function-set names
[ ] Convert "fs{0}" name-generation to "" generation
[ ] Fix any tests that fail

[ ] Code to output dict-based models from Imfit object
-- new method for Imfit (original suggestion was to alter getRawParameters() output;
probably better to have a new method)
[ ] Check for current code (or lack thereof) for outputting models
[ ] Write unit test to output dict version of model (test_fitting.py)

[ ] Code to instantiate Imfit object with model-dict
-- imfit_fitter = Imfit(model_dict)

[ ] Write documentation for dict-based models
[ ] Look at current docs to see where this would go



* Packaging:
Expand Down
18 changes: 11 additions & 7 deletions pyimfit/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,15 @@ def parse_config( lines: List[str] ) -> ModelDescription:
options = read_options(lines[block_start:i])
model.options.update(options)
else:
funcSetName = "fs{0:d}".format(functionBlock_id)
model.addFunctionSet(read_function_set(funcSetName, lines[block_start:i]))
# possible auto-label-generation code
# funcSetLabel = "fs{0:d}".format(functionBlock_id)
funcSetLabel = ""
model.addFunctionSet(read_function_set(funcSetLabel, lines[block_start:i]))
functionBlock_id += 1
block_start = i
funcSetName = "fs{0:d}".format(functionBlock_id)
model.addFunctionSet(read_function_set(funcSetName, lines[block_start:i + 1]))
# funcSetLabel = "fs{0:d}".format(functionBlock_id)
funcSetLabel = ""
model.addFunctionSet(read_function_set(funcSetLabel, lines[block_start:i + 1]))
return model


Expand Down Expand Up @@ -164,14 +167,15 @@ def read_options( lines: List[str] ) -> OrderedDict:



def read_function_set( name: str, lines: List[str] ) -> FunctionSetDescription:
def read_function_set( label: str, lines: List[str] ) -> FunctionSetDescription:
"""
Reads in lines of text corresponding to a function block (or 'set') containing
X0,Y0 coords and one or more image functions with associated parameter settings.
Parameters
----------
name : str
label : str
optional label for the function set (for "no particular label", use "")
lines : list of str
lines from configuration file
Expand All @@ -186,7 +190,7 @@ def read_function_set( name: str, lines: List[str] ) -> FunctionSetDescription:
y0 = read_parameter(lines[1])
if x0.name != x0_str or y0.name != y0_str:
raise ValueError('A function set must begin with the parameters X0 and Y0.')
fs = FunctionSetDescription(name)
fs = FunctionSetDescription(label)
fs.x0 = x0
fs.y0 = y0
block_start = 2
Expand Down
120 changes: 94 additions & 26 deletions pyimfit/descriptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,22 @@ def getStringDescription( self, noLimits=False, error: Optional[float]=None ):
return outputString


def getParamInfoList(self):
"""
Returns a list in one of three formats
1. [param-value]
2. [param-value, "fixed"] -- if parameter is fixed
3. [param-value, lower-limit, upper-limit] -- if parameter has limits
"""
output = [self._value]
if self._fixed:
output.append("fixed")
elif self.limits is not None:
output.append(self.limits[0])
output.append(self.limits[1])
return output


def __eq__(self, rhs):
if ((self._name == rhs._name) and (self._value == rhs._value)
and (self._limits == rhs._limits) and (self._fixed == rhs._fixed)):
Expand Down Expand Up @@ -415,6 +431,23 @@ def getStringDescription( self, noLimits=False, errors: Optional[Sequence[float]
return outputLines


def getFunctionAsDict(self):
"""
Returns a dict describing the function (suitable for use in e.g. dict_to_FunctionDescription())
Examples:
fDict = {'name': "Gaussian", 'label': "blob", 'parameters': p}
where p is a dict containing parameter names and values, e.g.
p = {'PA': 0.0, 'ell': 0.5, 'I_0': 100.0, 'sigma': 10.0}
OR
p = {'PA': [0.0, "fixed"], 'ell': [0.5, 0.1,0.8], 'I_0': [100.0, 10.0,1e3],
'sigma': [10.0, 5.0,20.0]}
"""
# FIXME: write code for this!
paramsDict = { param.name: param.getParamInfoList() for param in self._parameters }
funcDict = {'name': self._funcName, 'label': self._label, 'parameters': paramsDict}
return funcDict


def __eq__( self, rhs ):
if ((self._funcName == rhs._funcName) and (self._label == rhs._label)
and (self._parameters == rhs._parameters)):
Expand Down Expand Up @@ -446,17 +479,17 @@ class FunctionSetDescription(object):
Imfit image functions sharing a common (X0,Y0) position on the image.
This contains the X0 and Y0 coordinates, a list of FunctionDescription
objects, and name or label for the function set (e.g., "fs0", "star 1",
objects, and an optional label for the function set (e.g., "fs0", "star 1",
"galaxy 5", "offset nucleus", etc.)
Attributes
----------
name : str
name for the function set
label : str
label for the function set ("" = default no-name value)
_name : str
name for the function set
_label : str
label for the function set
x0 : ParameterDescription
x-coordinate of the function block/set's center
y0 : ParameterDescription
Expand Down Expand Up @@ -492,10 +525,10 @@ class FunctionSetDescription(object):
the function block/set (including X0,Y0)
"""
def __init__( self, name: str, x0param: Optional[ParameterDescription]=None,
def __init__( self, label: Optional[str]="", x0param: Optional[ParameterDescription]=None,
y0param: Optional[ParameterDescription]=None,
functionList: Optional[List[FunctionDescription]]=None ):
self._name = name
self._label = label
if x0param is None:
self.x0 = ParameterDescription('X0', 0.0)
else:
Expand All @@ -522,25 +555,30 @@ def __init__( self, name: str, x0param: Optional[ParameterDescription]=None,
@classmethod
def dict_to_FunctionSetDescription(cls, inputDict: dict):
"""
This is a convenience method to generate a ModelDescription object
from a standard Imfit configuration file.
This is a convenience method to generate a FunctionSetDescription object
from a dict
Parameters
----------
inputDict : dict
dict describing the function set
{'X0': list, 'Y0': list, 'function_list': [list of dicts describing functions]}
{'label': str, 'X0': list, 'Y0': list, 'function_list': [list of dicts describing functions]}
where "list" for X0 and Y0 is one of
[value]
[value, "fixed"]
[value, lower-limit, upper-limit]
Returns
-------
fset : :class:`FunctionSetDescription`
The function-set description.
"""

# extract name
# extract label
try:
fsName = inputDict['name']
fsLabel = inputDict['label']
except KeyError:
fsName = 'fs0'
fsLabel = ""
# extract x0,y0
x0y0_params = {}
for key in ['X0', 'Y0']:
Expand All @@ -559,15 +597,15 @@ def dict_to_FunctionSetDescription(cls, inputDict: dict):
nFuncs = len(inputDict['function_list'])
funcList = [FunctionDescription.dict_to_FunctionDescription(fdict) for fdict in
inputDict['function_list']]
return FunctionSetDescription(fsName, x0y0_params['X0'], x0y0_params['Y0'], funcList)
return FunctionSetDescription(fsLabel, x0y0_params['X0'], x0y0_params['Y0'], funcList)


@property
def name(self):
def label(self):
"""
Custom name/label for the function set.
"""
return self._name
return self._label


def addFunction(self, f: FunctionDescription):
Expand Down Expand Up @@ -690,8 +728,25 @@ def getStringDescription( self, noLimits=False, errors: Optional[Sequence[float]
return outputLines


def getFuncSetAsDict(self):
"""
Returns a dict describing the function set (suitable for use in e.g. dict_to_FunctionSetDescription())
{'X0': list, 'Y0': list, 'function_list': [list of dicts describing functions]}
{'label': str, 'X0': list, 'Y0': list, 'function_list': [list of dicts describing functions]}
"""
funcSetDict = {}
if self._label != "":
funcSetDict['label'] = self._label
funcSetDict['X0'] = self.x0.getParamInfoList()
funcSetDict['Y0'] = self.y0.getParamInfoList()
funcList = [ func.getFunctionAsDict() for func in self._functions ]
funcSetDict['function_list'] = funcList
return funcSetDict


def __eq__(self, rhs):
if ((self._name == rhs._name) and (self.x0 == rhs.x0) and (self.y0 == rhs.y0)
if ((self._label == rhs._label) and (self.x0 == rhs.x0) and (self.y0 == rhs.y0)
and (self._functions == rhs._functions)):
return True
else:
Expand All @@ -707,7 +762,7 @@ def __str__(self):


def __deepcopy__(self, memo):
fs = FunctionSetDescription(self._name)
fs = FunctionSetDescription(self._label)
fs.x0 = copy(self.x0)
fs.y0 = copy(self.y0)
fs._functions = deepcopy(self._functions, memo)
Expand Down Expand Up @@ -828,7 +883,7 @@ def dict_to_ModelDescription(cls, inputDict: dict):
inputDict : dict
dict describing the model, with one required entry -- "function_sets" --
and one optional entry -- "options"
"function_set_list" : list of dict, each one specifying a function set,
"function_sets" : list of dict, each one specifying a function set,
suitable as input to FunctionSetDescription.dict_to_FunctionSetDescription()
"options" : dict of {str: float} specifying image-description options
Expand Down Expand Up @@ -895,10 +950,11 @@ def addFunctionSet(self, fs: FunctionSetDescription):
"""
if not isinstance(fs, FunctionSetDescription):
raise ValueError('fs is not a FunctionSet object.')
if self._contains(fs.name):
raise KeyError('FunctionSet named %s already exists.' % fs.name)
if self._contains(fs.label):
raise KeyError('FunctionSet labeled %s already exists.' % fs.label)
self._functionSets.append(fs)
setattr(self, fs.name, fs)
if fs.label is not None:
setattr(self, fs.label, fs)
self.nFunctionSets += 1
self.nParameters += fs.nParameters

Expand Down Expand Up @@ -929,9 +985,9 @@ def replaceOptions( self, optionsDict: Dict[str,float] ):
self.options = optionsDict


def _contains(self, name: str):
def _contains(self, label: str):
for fs in self._functionSets:
if fs.name == name:
if fs.label == label:
return True
return False

Expand Down Expand Up @@ -994,12 +1050,12 @@ def functionLabelList(self):

def functionSetNameList(self):
"""
List of the function sets composing this model, as strings.
List of the functions composing this model, as strings, grouped by function set.
Returns
-------
func_set_list : list of list of string
List of the function sets: [[functions_in_set1], [functions_in_set2], ...]
List of the function names, grouped by function set: [[functions_in_set1], [functions_in_set2], ...]
"""
functionSetList = []
for function_set in self._functionSets:
Expand Down Expand Up @@ -1090,6 +1146,18 @@ def getStringDescription( self, noLimits=False, errors: Optional[Sequence[float]
return outputLines


def getModelAsDict( self ):
"""
Returns the model in dict form (suitable for use in e.g. dict_to_ModelDescription)
"""
modelDict = {}
fsetDictList = [function_set.getFuncSetAsDict() for function_set in self._functionSets ]
modelDict["function_sets"] = fsetDictList
if len(self.options) > 0:
modelDict["options"] = self.options
return modelDict


def __eq__(self, rhs):
if ((self.options == rhs.options) and (self._functionSets == rhs._functionSets)):
return True
Expand Down
13 changes: 13 additions & 0 deletions pyimfit/fitting.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,19 @@ def saveCurrentModelToFile( self, filename: str, includeImageOptions=False ):
outf.write(line)


def getModelAsDict(self):
"""
Returns current model (including parameter values) as a dict suitable for use
with ModelDescription.dict_to_ModelDescription class method.
Returns
-------
model_dict : dict
"""
model_desc = self.getModelDescription()
return model_desc.getModelAsDict()


def getRawParameters(self):
"""
Returns current model parameter values.
Expand Down

0 comments on commit 8d8ec1e

Please sign in to comment.