import json
import sys
import math
from dplus.metadata import meta_models, hardcode_models, _type_to_int, _models_with_files_index_dict, _int_to_type
[docs]class Constraints:
'''
The Constraints class contains the following properties:
* MaxValue: a float whose default value is infinity
* MinValue: a float whose default value is -infinity
'''
def __init__(self, max_val=math.inf, min_val=-math.inf, minindex=-1, maxindex=-1, link=-1):
try:
if max_val <= min_val:
raise ValueError("Constraints' upper bound must be greater than lower bound")
except TypeError: #for some reason, strings weren't being converted to numbers properly
if max_val=="inf":
max_val=math.inf
if min_val=="-inf":
min_val=-math.inf
if max_val <= min_val:
raise ValueError("Constraints' upper bound must be greater than lower bound")
self.MaxValue = max_val
self.MinValue = min_val
self.isConstrained= False
if max_val!=math.inf or min_val != -math.inf:
self.isConstrained=True
self.min_index=minindex
self.max_index=maxindex
self.link=link
[docs] @staticmethod
def from_dictionary(json):
"""
creates Constraints class instance with the json dictionary.
:param json: json dictionary
:return instance of Constraints class with the json data
"""
try:
c = Constraints(json["MaxValue"], json["MinValue"], json["MinIndex"], json["MaxIndex"], json["Link"])
except KeyError: #backwards compatibility with older version of constraints
c=Constraints(json["MaxValue"], json["MinValue"])
return c
[docs] def serialize(self):
"""
saves the contents of a class to a dictionary.
:return: dictionary of the class fields (isConstrained, consMin and consMax)
"""
return {
"Link": self.link,
"MaxIndex": self.max_index,
"MaxValue": self.MaxValue,
"MinIndex": self.min_index,
"MinValue": self.MinValue
}
[docs]class Parameter:
'''
The Parameter class contains the following properties:
* value: a float whose default value is 0
* sigma: a float whose default value is 0
* mutable: a boolean whose default value is False
* constraints: an instance of the Constraints class, by default it is the default Constraints
'''
def __init__(self, value=0, sigma=0, mutable=False, constraints=Constraints()):
try:
self.value = float(value)
self.sigma = float(sigma)
except:
raise ValueError("non-number value creeping into param" + str(value) + " " + str(sigma))
self.mutable = mutable
self.constraints = constraints
@property
def isConstrained(self):
'''
check if there are constrains. Return True is there is at least on constrain value.
:return: True
'''
if self.constraints.link != -1:
return True
if self.constraints.max_index != -1:
return True
if self.constraints.MaxValue != math.inf:
return True
if self.constraints.min_index != -1:
return True
if self.constraints.MinValue != -math.inf:
return True
return False
[docs] def serialize(self):
"""
saves the contents of a class to a dictionary. unlike other serialize methods, not used in creating ParamterTree
to send to D+ Calculation. Serialized parameters are expected by D+ as a *result* of fitting.
:return: dictionary of the class fields (Value, isMutable, consMinIndex,consMaxIndex, linkIndex, sigma and constraints)
"""
return {"Value": self.value,
"isMutable": self.mutable,
"isConstrained": self.isConstrained,
"consMin": self.constraints.MinValue,
"consMax": self.constraints.MaxValue,
"consMinIndex": self.constraints.min_index,
"consMaxIndex": self.constraints.max_index,
"linkIndex": self.constraints.link,
"sigma": self.sigma}
def __str__(self):
return str(self.serialize())
def __repr__(self):
return str(self.serialize())
[docs]class Model:
'''
A base class to D+ models.
'''
_model_ptr_index = 0
def __init__(self):
self.name = ""
self.use_grid = True
self.model_ptr = Model._model_ptr_index
Model._model_ptr_index += 1
self.extra_params={}
self.extra_param_index_map = []
self.location_params={}
self.location_param_index_map = ["x", "y", "z", "alpha", "beta", "gamma"]
self._init_from_metadata()
def _init_from_metadata(self):
#location params:
location_vals = ["x", "y", "z","alpha", "beta", "gamma"]
for val in location_vals:
self.location_params[val] = Parameter()
# extra params:
try:
e_params = self.metadata["extraParams"]
except: #nothing to do here
return
for index, param in enumerate(e_params):
self.extra_param_index_map.append(param["name"])
self.extra_params[param["name"]] = Parameter(value=param["defaultValue"])
[docs] def serialize(self):
"""
saves the contents of a class to a dictionary.
:return: dictionary of the class fields.
"""
mydict = {"ModelPtr": self.model_ptr, "Name": self.name, "Use_Grid": self.use_grid,
"nExtraParams": len(self.extra_params),
"nLayers": 0, "nlp": 0, # this is default, overwritten by modelWithLayers
"Type": _int_to_type(
# for now, type must be proceeded with comma because we haven't gotten rid of containers yet
self.index), #self.index is set in the factory
"Mutables": [],
"Parameters": [],
"Sigma": [],
"Constraints": [],
"ExtraParameters": [], "ExtraConstraints": [], "ExtraMutables": [], "ExtraSigma": [],
"Location": {}, "LocationConstraints": {}, "LocationMutables": {}, "LocationSigma": {}
}
# extraparams
for i, param_name in enumerate(self.extra_param_index_map):
param = self.extra_params[param_name]
mydict["ExtraParameters"].append(param.value)
mydict["ExtraConstraints"].append(param.constraints.serialize())
mydict["ExtraMutables"].append(param.mutable)
mydict["ExtraSigma"].append(param.sigma)
# locationparams
for param_name in self.location_params:
param = self.location_params[param_name]
mydict["Location"][param_name] = param.value
mydict["LocationConstraints"][param_name] = param.constraints.serialize()
mydict["LocationMutables"][param_name] = param.mutable
mydict["LocationSigma"][param_name] = param.sigma
return mydict
def __str__(self):
return (str(self.serialize()))
[docs] def load_from_dictionary(self, json):
'''
sets the values of the various fields within a class to match those contained within a suitable dictionary.
:param json: json dictionary
'''
# first, check that the type matches the model's type index and everything is in order
# Domains and populations don't have metadata, their type_index is -1, skip this section
if self.index == -1:
pass
else:
type_index = _type_to_int(json["Type"])
if type_index != self.index:
raise ValueError("Model type index mismatch")
# override instance values
try:
self.name = json["Name"]
except KeyError:
pass # we don't require names
self.model_ptr = json["ModelPtr"]
self.use_grid = json.get("Use_Grid", False)
for param_index in range(len(json.get("ExtraParameters", []))):
param = Parameter(value=json["ExtraParameters"][param_index], mutable=json["ExtraMutables"][param_index],
sigma=json["ExtraSigma"][param_index],
constraints=Constraints.from_dictionary(json["ExtraConstraints"][param_index]))
self.extra_params[self.extra_param_index_map[param_index]] = param
for param_index in json.get("Location", []):
param = Parameter(value=json["Location"][param_index], mutable=json["LocationMutables"][param_index],
sigma=json["LocationSigma"][param_index],
constraints=Constraints.from_dictionary(json["LocationConstraints"][param_index]))
self.location_params[param_index] = param
[docs] def get_mutable_params(self):
'''
used in combining fitting results, or running fitting from within python
:return: returns all the mutables params in extra_params and location_params
'''
mut_array = []
# location params
for param_name in self.location_param_index_map:
if self.location_params[param_name].mutable:
mut_array.append(self.location_params[param_name])
# mutable params
for param_name in self.extra_param_index_map:
if self.extra_params[param_name].mutable:
mut_array.append(self.extra_params[param_name])
return mut_array
[docs] def set_mutable_params(self, mut_arr):
'''
receives an order array of mutable params and set the values in extra_params and location_params according to that array
:param mut_arr: array of mutable params
'''
param_index=0
for param_name in self.location_param_index_map:
if self.location_params[param_name].mutable:
self.location_params[param_name].value=mut_arr[param_index]
param_index+=1
for param_name in self.extra_param_index_map:
if self.extra_params[param_name].mutable:
self.extra_params[param_name].value=mut_arr[param_index]
param_index+=1
# location params
def __basic_json_params(self):
'''
:return: a dictionary in the form:
{
"ModelPtr": self.model_ptr,
"Parameters": params,
"Submodels": []
}
submodels contains an array of this exact dictionary for child models.
Parameters is an array of parameters, always in the following order:
* x
* y
* z
* alpha
* beta
* gamma
* useGrid
* number of layers
* params[i][j]
...
...
* extraparams[i]
...
'''
params = []
# add default location params
# add location params
location_vals = ["x", "y", "z", "alpha", "beta", "gamma"]
for val in location_vals:
try:
params.append(self.location_params[val].serialize())
except: #if we don't have location params, no big, just attach defaults
params.append(Parameter().serialize())
# add useGrid
if self.use_grid:
params.append(Parameter(1).serialize())
else:
params.append(Parameter(0).serialize())
# add number of layers
params.append(Parameter(1).serialize())
# add extra params
for param in self.extra_param_index_map:
params.append(self.extra_params[param].serialize())
return {
"ModelPtr": self.model_ptr,
"Parameters": params,
"Submodels": []
}
[docs]class ModelWithChildren(Model):
'''
D+ has few models which can have children. For example: Domain, population and Symmetry models
'''
def __init__(self):
self.Children = []
super().__init__()
[docs] def serialize(self):
'''
saves the contents of a class to a dictionary.
:return: dictionary of the class fields.
'''
mydict = super().serialize()
mydict.update(
{
"Children": [child.serialize() for child in self.Children]
}
)
return mydict
def __str__(self):
return (str(self.serialize()))
[docs] def load_from_dictionary(self, json):
'''
sets the values of the various fields within a class to match those contained within a suitable dictionary.
:param json: json dictionary
'''
super().load_from_dictionary(json)
for child in json["Children"]:
childmodel = ModelFactory.create_model_from_dictionary(child)
self.Children.append(childmodel)
def __basic_json_params(self):
basic_dict = super().__basic_json_params()
for child in self.Children:
basic_dict["Submodels"].append(child.__basic_json_params())
return basic_dict
[docs]class ModelWithLayers(Model):
'''
D+ has few models which can have layers. For example: Sphere, Helix and UniformHollowCylinder
'''
def __init__(self):
self.layer_params = []
super().__init__()
def _init_from_metadata(self):
super()._init_from_metadata()
# layer params:
layerinfo = self.metadata["layers"]["layerInfo"]
params = self.metadata["layers"]["params"]
for layer in layerinfo:
layer_dict = {}
for param_index, parameter in enumerate(params):
if layer["index"] == -1:
# This is just an indication of the default layer when more layers are added
# it is not an actual layer.
self._default_layer = Parameter(value=layer["defaultValues"][param_index])
else:
layer_dict[parameter] = Parameter(value=layer["defaultValues"][param_index])
if layer["index"] != -1:
self.layer_params.append(layer_dict)
self.layer_param_index_map = self.metadata["layers"]["params"]
[docs] def parameters_to_json_arrays(self):
json_dict = {"Parameters": [], "Constraints": [], "Mutables": [], "Sigma": [] }
# layerparams
for layer in self.layer_params:
param_array = []
constr_array = []
mut_array = []
sigma_array = []
for i, param_name in enumerate(self.layer_param_index_map):
param = layer[param_name]
param_array.append(param.value)
constr_array.append(param.constraints.serialize())
mut_array.append(param.mutable)
sigma_array.append(param.sigma)
json_dict["Parameters"].append(param_array)
json_dict["Constraints"].append(constr_array)
json_dict["Mutables"].append(mut_array)
json_dict["Sigma"].append(sigma_array)
# some additional things that are necessary
json_dict["nlp"] = len(self.layer_params[0])
json_dict["nLayers"] = len(self.layer_params)
return json_dict
[docs] def load_from_dictionary(self, json):
'''
sets the values of the various fields within a class to match those contained within a suitable dictionary.
:param json: json dictionary
'''
super().load_from_dictionary(json)
for layer_index in range(len(json["Parameters"])):
for param_index in range(len(json["Parameters"][layer_index])):
param = Parameter(value=json["Parameters"][layer_index][param_index],
mutable=json["Mutables"][layer_index][param_index],
sigma=json["Sigma"][layer_index][param_index],
constraints=Constraints.from_dictionary(json["Constraints"][layer_index][param_index]))
try:
self.layer_params[layer_index][self.layer_param_index_map[param_index]] = param
except IndexError:
if len(json["Parameters"]) > self.metadata["layers"]["max"] and self.metadata["layers"][
"max"] != -1:
raise ValueError(
"Not allowed to set more than " + str(self.metadata["layers"]["max"]) + " layers")
# otherwise go ahead and add the layer
self.layer_params.append({})
self.layer_params[layer_index][self.layer_param_index_map[param_index]] = param
[docs] def serialize(self):
'''
saves the contents of a class to a dictionary.
:return: dictionary of the class fields.
'''
mydict = super().serialize()
mydict.update(
self.parameters_to_json_arrays()
)
return mydict
[docs] def get_mutable_params(self):
'''
Return all the mutable params of the model. The mutable params come from the layers array, extra_params and location_params.
:return: mutable params array
'''
mut_array = []
# location params
for param_name in self.location_param_index_map:
if self.location_params[param_name].mutable:
mut_array.append(self.location_params[param_name])
# layer params
for layer in self.layer_params:
for param_name in layer:
if layer[param_name].mutable:
mut_array.append(layer[param_name])
# extra params
for param_name in self.extra_param_index_map:
if self.extra_params[param_name].mutable:
mut_array.append(self.extra_params[param_name])
return mut_array
[docs] def set_mutable_params(self, mut_array):
'''
receives an order array of mutable params and set the values in layer , extra_params and location_params according to that array.
:param mut_arr: array of mutable params
'''
index = 0
# location params
for param_name in self.location_param_index_map:
if self.location_params[param_name].mutable:
self.location_params[param_name] = mut_array[index]
index += 1
# layer params
for layer in self.layer_params:
for param_name in layer:
if layer[param_name].mutable:
layer[param_name] = mut_array[index]
index += 1
# extra params
for param_name in self.extra_param_index_map:
if self.extra_params[param_name].mutable:
self.extra_params[param_name] = mut_array[index]
index += 1
def __basic_json_params(self):
'''
:param use_grid:
:return:
x
y
z
alpha
beta
gamma
useGrid
number of layers
params[i][j]
...
...
extraparams[i]
...
'''
# basic_dict = super().__basic_json_params(useGrid)
# override basic entirely
basic_dict= super().__basic_json_params()
super_params_arr=basic_dict["Parameters"]
#the first 7 params are location and use_grid and remain unchanged. The rest are overwritten
params=super_params_arr[:7]
# add number of layers
params.append(Parameter(len(self.layer_params)).serialize())
# add params:
for param in self.layer_param_index_map:
for layer in self.layer_params:
params.append(layer[param].serialize())
# add extra params
for param in self.extra_param_index_map:
params.append(self.extra_params[param].serialize())
basic_dict["Parameters"] = params
return basic_dict
[docs]class ModelWithFile(Model):
'''
D+ has few models which have a file. For example: PDB, AMP and ScriptedSymmetry
'''
def __init__(self, filename=""):
self.filenames = []
self.filename = filename
super().__init__()
[docs] def serialize(self):
'''
saves the contents of a class to a dictionary.
:return: dictionary of the class fields.
'''
mydict = super().serialize()
mydict.update(
{
"Filename": self.filename,
}
)
try:
mydict.update(
{
"Centered": self.centered,
}
)
except (AttributeError, KeyError) as err: # not everything has centered
pass
try:
mydict.update(
{
"AnomFilename": self.anomfilename,
}
)
except (AttributeError, KeyError) as err: # not everything has an anomfilename
pass
return mydict
def __str__(self):
return (str(self.serialize()))
[docs] def load_from_dictionary(self, json):
'''
sets the values of the various fields within a class to match those contained within a suitable dictionary.
:param json: json dictionary
'''
super().load_from_dictionary(json)
self.filename = json["Filename"]
self.filenames.append(self.filename)
# TODO: various optional additonal fields that really should be handled in a better way
try:
self.centered = json["Centered"]
except (AttributeError, KeyError) as err: # not everything has centered
pass
try:
self.anomfilename = json["AnomFilename"]
self.filenames.append(json["AnomFilemame"])
except (AttributeError, KeyError) as err: # not everything has an anomfilename
pass
def _get_model_tuple(metadata):
model_list = []
if "isLayerBased" in metadata:
if metadata["isLayerBased"] == True:
model_list.append(ModelWithLayers)
if metadata["category"] == 9: # symmetry
model_list.append(ModelWithChildren)
if metadata["name"] in _models_with_files_index_dict:
model_list.append(ModelWithFile)
if len(model_list)==0:
model_list=[Model]
return tuple(model_list)
[docs]class ScriptedSymmetry(Model):
'''
A class for D+ ScriptedSymmetry, this is sufficient for running against existing backend,\
but does NOT implement running with python fit
'''
# TODO: this is sufficient for running against existing backend, but does NOT implement running with python fit
def __init__(self, **fields):
self.__dict__.update(fields)
[docs] def load_from_dictionary(self, json):
'''
sets the values of the various fields within a class to match those contained within a suitable dictionary.
:param json: json dictionary
'''
# print(vars(self))
self.json = json
if "Children" in json:
self.Children = []
for child in json["Children"]:
childmodel = ModelFactory.create_model_from_dictionary(child)
self.Children.append(childmodel)
[docs] def serialize(self):
'''
saves the contents of a class to a dictionary.
:return: dictionary of the class fields.
'''
return_dict = {}
for key in self.json:
return_dict[key] = self.__dict__[key]
if "Children" in self.json:
return_dict["Children"] = [child.serialize() for child in self.Children]
return return_dict
class ModelFactory:
models_arr = []
from types import ModuleType
models = ModuleType('dplus.DataModels.models')
sys.modules['dplus.DataModels.models'] = models
@classmethod
def add_model(cls, metadata):
no_space_name = "".join(metadata["name"].split())
no_space_name = no_space_name.replace("-", "")
modeltuple = _get_model_tuple(metadata)
# replace name with type_name
metadata["type_name"] = metadata.pop("name")
metadata["metadata"] = metadata.copy()
myclass = type(no_space_name, modeltuple,
metadata)
ModelFactory.models_arr.append(myclass)
setattr(ModelFactory.models, no_space_name, myclass)
@classmethod
def create_model_from_dictionary(cls, json):
model_index_str = json["Type"]
if model_index_str in ["Scripted Geometry", "Scripted Model"]:
raise NotImplemented(
"Tal says:Scripted models and geometries are remnants of a yet unimplemented feature (script models, e.g., written in Python). They should be obliterated from existence for now, only to be revived if python models work.")
if model_index_str == "Scripted Symmetry":
m = ScriptedSymmetry(**json)
m.load_from_dictionary(json)
return m
model_index = _type_to_int(model_index_str)
for model in ModelFactory.models_arr: # TODO: Turn this into a dictionary at some point
if model.index == model_index:
m = model()
m.load_from_dictionary(json)
return m
raise ValueError("Model not found")
@classmethod
def create_model(cls, name_or_index):
no_space_name = "_".join(name_or_index.split())
for model in ModelFactory.models_arr:
if model.type_index == name_or_index or model.type_name == name_or_index or model.type_name == no_space_name:
return model
raise ValueError("Model not found")
[docs]class Population(ModelWithChildren):
'''
`Population` can contain a number of `Model` classes. Some models have children, which are also models.
'''
index = -1
def __init__(self):
super().__init__()
self.population_size = 1
self.population_size_mut = False
self.extra_param_index_map=["Population Size"]
self.extra_params["Population Size"]=Parameter(value=self.population_size,
mutable=self.population_size_mut)
@property
def models(self):
'''
Return all the models in the population class.
:return: models array
'''
return self.Children
[docs] def add_model(self, model):
'''
:param model: model to add to the population
'''
self.models.append(model)
[docs] def serialize(self):
"""
saves the contents of a class Population to a dictionary.
:return: dictionary of the class fields.
"""
mydict = super().serialize()
mydict["Models"] = mydict.pop("Children")
newdict = {
"PopulationSize": self.population_size,
"PopulationSizeMut": self.population_size_mut,
"ModelPtr": self.model_ptr,
"Models": mydict["Models"]
}
return newdict
[docs] def load_from_dictionary(self, json):
'''
sets the values of the various fields within a class to match those contained within a suitable dictionary.
:param json: json dictionary
'''
self.model_ptr = json["ModelPtr"]
for model in json["Models"]:
self.Children.append(ModelFactory.create_model_from_dictionary(model))
self.population_size = json["PopulationSize"]
self.population_size_mut = json["PopulationSizeMut"]
self.extra_params["Population Size"]=Parameter(value=self.population_size,
mutable=self.population_size_mut)
[docs]class Domain(ModelWithChildren):
'''
The Domain class describes the parameter tree.
The Domain model is the root of the parameter tree, which can contain multiple populations.
'''
index = -1
def __init__(self):
super().__init__()
self.scale = 1
self.constant = 0.0
self.scale_mut = False
self.constant_mut = False
self.geometry = "Domains"
self.populations.append(Population())
self.extra_param_index_map=["Scale", "Constant"]
self.extra_params["Constant"]=Parameter(value=self.constant, mutable=self.constant_mut)
self.extra_params["Scale"]=Parameter(value=self.scale, mutable=self.scale_mut)
@property
def populations(self):
'''
:return: The populations of the domain
'''
return self.Children
[docs] def serialize(self):
"""
saves the contents of a class Domain to a dictionary.
:return: dictionary of the class fields.
"""
# we need to completely override the dictionary returned by model
# (which includes nlayers and other extraneous fields).
mydict = super().serialize()
mydict["Populations"] = mydict.pop("Children")
newdict = {
"ModelPtr": self.model_ptr,
"Scale": self.scale,
"ScaleMut": self.scale_mut,
"Constant": self.constant,
"ConstantMut": self.constant_mut,
"Geometry": self.geometry,
"Populations": mydict["Populations"]
}
return newdict
[docs] def load_from_dictionary(self, json):
"""
sets the values of the various fields within a class to match those contained within a suitable dictionary.
:param json: json dictionary
"""
self.populations[:]=[] #by default Domain creates an empty population. However if we are loading from json we don't want this empty population
for population in json["Populations"]:
popu = Population()
popu.load_from_dictionary(population)
self.Children.append(popu)
self.scale = json["Scale"]
self.scale_mut = json["ScaleMut"]
try:
self.constant = json["Constant"] #TODO: add back if necessary
self.constant_mut = json["ConstantMut"]
except Exception as e:
print(e) #is probably an old model without constant
self.geometry = json["Geometry"]
self.model_ptr = json["ModelPtr"]
self.extra_params["Constant"]=Parameter(value=self.constant, mutable=self.constant_mut)
self.extra_params["Scale"]=Parameter(value=self.scale, mutable=self.scale_mut)
def __basic_json_params(self, useGrid):
'''
:param useGrid:
:return:
'''
self.use_grid = useGrid
basic_dict = super().__basic_json_params(useGrid)
# we need to add in parameters to the domain
basic_dict["Parameters"].append(self.scale_param.serialize())
for population in self.Children:
basic_dict["Parameters"].append(population.population_size_param.serialize())
return basic_dict
for model in hardcode_models:
ModelFactory.add_model(model)
for model in meta_models:
ModelFactory.add_model(model)