def test_add_default_quantities(self): material = Material(add_default_quantities=True) self.assertEqual(list(material['temperature'])[0], QuantityFactory.create_quantity("temperature", 300, provenance=ProvenanceElement(model='default'))) self.assertEqual(list(material['relative_permeability'])[0], QuantityFactory.create_quantity("relative_permeability", 1, provenance=ProvenanceElement(model='default')))
def test_get_weight(self): q1 = Quantity("band_gap", 3.2) wt = get_weight(q1) self.assertEqual(wt, 1) p2 = ProvenanceElement(model="model_2", inputs=[q1]) q2 = Quantity("refractive_index", 4, provenance=p2) wt2 = get_weight(q2, {"model_2": 0.5}) self.assertEqual(wt2, 0.5) p3 = ProvenanceElement(model="model_3", inputs=[q2]) q3 = Quantity("bulk_modulus", 100, provenance=p3) wt3 = get_weight(q3, {"model_3": 0.25, "model_2": 0.5}) self.assertEqual(wt3, 0.125)
def get_materials_for_mpids(self, mpids, filter_null_properties=True): """ Retrieve a list of Materials from the materials Project for a given list of Materials Project IDs. Args: mpids: a list of Materials Project IDs Returns: """ materials_properties = self.get_properties_for_mpids( mpids, filter_null_properties=filter_null_properties) materials = [] for material_properties in materials_properties: material = Material() for property_name, property_value in material_properties.items(): provenance = ProvenanceElement(source='Materials Project') quantity = Quantity(self.mapping[property_name], property_value, provenance=provenance) material.add_quantity(quantity) materials.append(material) return materials
def __init__(self, symbol_type, value, tags=None, provenance=None): """ Parses inputs for constructing a BaseQuantity object. Args: symbol_type (Symbol or str): pointer to a Symbol object in DEFAULT_SYMBOLS or string giving the name of a Symbol object. Identifies the type of data stored in the quantity. value (id): value of the quantity. tags (list<str>): list of strings storing metadata from evaluation. provenance (ProvenanceElement): provenance associated with the object (e. g. inputs, model, see ProvenanceElement). If not specified, a default object will be created. All objects will receive the time created and the internal ID as fields 'source.date_created' and 'source.source_key', respectively, if the fields are not already written. """ if not isinstance(symbol_type, Symbol): symbol_type = self.get_symbol_from_string(symbol_type) if provenance and not isinstance(provenance, ProvenanceElement): raise TypeError("Expected ProvenanceElement for provenance. " "Instead received: {}".format(type(provenance))) self._value = value self._symbol_type = symbol_type self._tags = [] if tags: if isinstance(tags, str): tags = [tags] self._tags.extend(tags) self._provenance = provenance self._internal_id = uuid.uuid4().hex if self._provenance is not None: if not isinstance(self._provenance.source, dict): self._provenance.source = {"source": self._provenance.source} if 'date_created' not in self._provenance.source.keys() or \ self._provenance.source['date_created'] in (None, ""): self._provenance.source['date_created'] = datetime.now( ).strftime("%Y-%m-%d %H:%M:%S") if 'source_key' not in self._provenance.source.keys() or \ self._provenance.source['source_key'] in (None, ""): self._provenance.source['source_key'] = self._internal_id else: self._provenance = ProvenanceElement( source={ "source": None, "source_key": self._internal_id, "date_created": datetime.now().strftime( "%Y-%m-%d %H:%M:%S") })
def from_weighted_mean(cls, quantities): """ Function to invoke weighted mean quantity from other quantities Args: quantities ([NumQuantity]): list of quantities of the same type Returns: (NumQuantity) a quantity containing the weighted mean and standard deviation. """ if not all(isinstance(q, cls) for q in quantities): raise ValueError( "Weighted mean cannot be applied to non-NumQuantity objects") input_symbol = quantities[0].symbol if not all(input_symbol == q.symbol for q in quantities): raise ValueError("Can only calculate a weighted mean if " "all quantities refer to the same symbol.") # TODO: an actual weighted mean; just a simple mean at present # TODO: support propagation of uncertainties (this will only work # once at present) # # TODO: test this with units, not magnitudes ... remember units # # may not be canonical units(?) # if isinstance(quantities[0].value, list): # # hack to get arrays working for now # vals = [q.value for q in quantities] # else: # vals = [q.value.magnitude for q in quantities] vals = [q.value for q in quantities] # Explicit formulas for mean / standard dev for pint support new_value = sum(vals) / len(vals) std_dev = (sum([(v - new_value)**2 for v in vals]) / len(vals))**(1 / 2) # Accumulate provenance and tags for new quantities new_tags = set() new_provenance = ProvenanceElement(model='aggregation', inputs=[]) for quantity in quantities: if quantity.tags: for tag in quantity.tags: new_tags.add(tag) new_provenance.inputs.append(quantity) return cls(symbol_type=input_symbol, value=new_value, tags=list(new_tags), provenance=new_provenance, uncertainty=std_dev)
def from_default(symbol): """ Method to invoke a default quantity from a symbol name Args: symbol (Symbol or str): symbol or string corresponding to the symbol name Returns: BaseQuantity corresponding to default quantity from default """ val = Registry("symbol_values").get(symbol) if val is None: raise ValueError("No default value for {}".format(symbol)) prov = ProvenanceElement(model='default') return QuantityFactory.create_quantity(symbol, val, provenance=prov)
def from_default(cls, symbol): """ Class method to invoke a default quantity from a symbol name Args: symbol (Symbol or str): symbol or string corresponding to the symbol name Returns: Quantity corresponding to default quantity from default """ val = DEFAULT_SYMBOL_VALUES.get(symbol) if val is None: raise ValueError("No default value for {}".format(symbol)) prov = ProvenanceElement(model='default', inputs=[]) return cls(symbol, val, provenance=prov)
def to_provenance_element(self, lookup=None): """ Converts the current object to a ProvenanceElement object, looking up missing input values if needed. Args: lookup: (dict or function) lookup container for missing provenance input information Returns: (ProvenanceElement) reconstructed provenance object """ if self._inputs: inputs = [v.to_quantity(lookup=lookup) for v in self._inputs] else: inputs = None return ProvenanceElement(model=self.model, inputs=inputs, source=self.source)
def get_materials_for_mpids(self, mpids, filter_null_values=True): """ Retrieve a list of Materials from the materials Project for a given list of Materials Project IDs. Args: mpids: a list of Materials Project IDs Returns: """ materials_quantities = self.get_quantities_for_mpids( mpids, filter_null_values=filter_null_values, include_date_created=True) materials = [] for material_quantities in materials_quantities: material = Material() try: date_created = material_quantities.pop('created_at') except KeyError: date_created = None for symbol_name, value in material_quantities.items(): provenance = ProvenanceElement( source={ 'source': 'Materials Project', 'source_key': material_quantities.get( 'material_id', None), 'date_created': date_created }) quantity = QuantityFactory.create_quantity( self.mapping[symbol_name], value, units=Registry("units").get(self.mapping[symbol_name], None), provenance=provenance) material.add_quantity(quantity) materials.append(material) return materials
def transform_properties_to_material(self, material_data): """ Produces a propnet Material object from a dictionary of AFLOW materials data. Args: material_data (dict): AFLOW materials data, keyed by AFLOW keyword, as Python native types, not as strings as they are stored in AFLOW. Returns: propnet.core.materials.Material: propnet material containing the AFLOW data """ qs = [] auid = material_data.get('auid') date_created = material_data.get('aflowlib_date') if date_created: date, tz = date_created.rsplit("GMT", 1) tz = "GMT{:+05d}".format(int(tz) * 100) date_object = datetime.strptime(date + tz, "%Y%m%d_%H:%M:%S_%Z%z") date_created = date_object.strftime("%Y-%m-%d %H:%M:%S") for prop, value in material_data.items(): if value is not None and prop in self.mapping: provenance = ProvenanceElement( source={'source': 'AFLOW', 'source_key': auid, 'date_created': date_created} ) if prop in self.transform_func: value = self.transform_func[prop](value) if value is None: continue q = QuantityFactory.create_quantity( self.mapping.get(prop), value, units=self.unit_map.get(prop), provenance=provenance ) qs.append(q) return Material(qs)
def process(self, item): if self.graph_parallel and not self.allow_child_process and \ current_process().name != "MainProcess": logger.warning( "It appears derive_quantities() is running " "in a child process, possibly in a parallelized " "Runner.\nThis is not recommended and will deteriorate " "performance.") # Define quantities corresponding to materials doc fields # Attach quantities to materials item = MontyDecoder().process_decoded(item) logger.info("Populating material for %s", item['task_id']) material = Material() if 'created_at' in item.keys(): date_created = item['created_at'] else: date_created = None provenance = ProvenanceElement( source={ "source": self.source_name, "source_key": item['task_id'], "date_created": date_created }) for mkey, property_name in self.materials_symbol_map.items(): value = pydash.get(item, mkey) if value: material.add_quantity( QuantityFactory.create_quantity( property_name, value, units=Registry("units").get(property_name, None), provenance=provenance)) # Add custom things, e. g. computed entry computed_entry = get_entry(item) if computed_entry: material.add_quantity( QuantityFactory.create_quantity("computed_entry", computed_entry, provenance=provenance)) else: logger.info("Unable to create computed entry for {}".format( item['task_id'])) material.add_quantity( QuantityFactory.create_quantity("external_identifier_mp", item['task_id'], provenance=provenance)) input_quantities = material.symbol_quantities_dict # Use graph to generate expanded quantity pool logger.info("Evaluating graph for %s", item['task_id']) new_material = self._graph_evaluator.evaluate( material, timeout=self.graph_timeout) # Format document and return logger.info("Creating doc for %s", item['task_id']) # Gives the initial inputs that were used to derive properties of a # certain material. doc = { "inputs": [ StorageQuantity.from_quantity(q) for q in chain.from_iterable(input_quantities.values()) ] } for symbol, quantities in new_material.symbol_quantities_dict.items(): # If no new quantities of a given symbol were derived (i.e. if the initial # input quantity/ies is/are the only one/s listed in the new material) then don't add # that quantity to the propnet entry document as a derived quantity. if len(quantities) == len(input_quantities[symbol]): continue sub_doc = {} try: # Write out all quantities as dicts including the # internal ID for provenance tracing qs = [ jsanitize(StorageQuantity.from_quantity(q), strict=True) for q in quantities ] except AttributeError as ex: # Check to see if this is an error caused by an object # that is not JSON serializable msg = ex.args[0] if "object has no attribute 'as_dict'" in msg: # Write error to db and logger errmsg = "Quantity of Symbol '{}' is not ".format(symbol.name) + \ "JSON serializable. Cannot write quantities to database!" logger.error(errmsg) sub_doc['error'] = errmsg qs = [] else: # If not, re-raise the error raise ex sub_doc['quantities'] = qs doc[symbol.name] = sub_doc aggregated_quantities = new_material.get_aggregated_quantities() for symbol, quantity in aggregated_quantities.items(): if symbol.name not in doc: # No new quantities were derived continue # Store mean and std dev for aggregated quantities sub_doc = { "mean": unumpy.nominal_values(quantity.value).tolist(), "std_dev": unumpy.std_devs(quantity.value).tolist(), "units": quantity.units.format_babel() if quantity.units else None, "title": quantity.symbol.display_names[0] } # Symbol Name -> Sub_Document, listing all Quantities of that type. doc[symbol.name].update(sub_doc) doc.update({ "task_id": item["task_id"], "pretty_formula": item.get("pretty_formula"), "deprecated": item.get("deprecated", False) }) if self.include_sandboxed: doc.update({'sbxn': item.get("sbxn", [])}) return jsanitize(doc, strict=True)
def setUp(self): # Inspiration was taken from the GraphTest class # I tried to construct the dictionaries for comparison # without writing out every one explicity by reusing # information where it was applicable. # If this is too unreadable, can change to writing it # out explicity in a JSON file and importing it. Would # still need to replace some fields dynamically. symbols = StorageTest.generate_symbols() self.custom_syms_as_dicts = { k: {'@module': 'propnet.core.symbols', '@class': 'Symbol', 'name': k, 'display_names': [k], 'display_symbols': [k], 'units': (1, ()), 'shape': 1, 'object_type': None, 'comment': None, 'category': 'property', 'constraint': None, 'default_value': None} for k in ['A', 'B', 'C'] } self.custom_syms_as_dicts['C'].update( {"units": None, "shape": None, "object_type": "str", "category": "object"}) self.custom_symbols_json = copy.deepcopy(self.custom_syms_as_dicts) for k in ['A', 'B']: self.custom_symbols_json[k]['units'] = [1, []] a = [QuantityFactory.create_quantity(symbols['A'], 19), QuantityFactory.create_quantity(symbols['A'], 23)] b = [QuantityFactory.create_quantity(symbols['B'], 38, provenance=ProvenanceElement(model='model1', inputs=[a[0]])), QuantityFactory.create_quantity(symbols['B'], 46, provenance=ProvenanceElement(model='model1', inputs=[a[1]]))] self.quantities_custom_symbol = {"A": a, "B": b} self.sq_custom_sym_as_dicts = { k: [{'@module': 'propnet.dbtools.storage', '@class': 'StorageQuantity', 'internal_id': vv._internal_id, 'data_type': 'NumQuantity', 'symbol_type': symbols[k], 'value': vv.magnitude, 'units': 'dimensionless', 'provenance': ProvenanceStore.from_provenance_element(vv.provenance), 'tags': [], 'uncertainty': None} for vv in v] for k, v in self.quantities_custom_symbol.items() } provenances_json = { "A": [{'@module': 'propnet.dbtools.storage', '@class': 'ProvenanceStore', 'model': None, 'inputs': None, 'source': aa.provenance.source} for aa in a]} provenances_json['B'] = [ {'@module': 'propnet.dbtools.storage', '@class': 'ProvenanceStore', 'model': 'model1', 'inputs': [{'@module': 'propnet.dbtools.storage', '@class': 'ProvenanceStoreQuantity', 'data_type': 'NumQuantity', 'symbol_type': self.custom_symbols_json['A'], 'internal_id': q.provenance.inputs[0]._internal_id, 'tags': [], 'provenance': p}], 'source': q.provenance.source} for q, p in zip(b, provenances_json['A'])] self.sq_custom_sym_json = copy.deepcopy(self.sq_custom_sym_as_dicts) for sym in ['A', 'B']: for q, p in zip(self.sq_custom_sym_json[sym], provenances_json[sym]): q['symbol_type'] = self.custom_symbols_json[sym] q['provenance'] = p band_gaps = [QuantityFactory.create_quantity('band_gap', 3.3, 'eV'), QuantityFactory.create_quantity('band_gap', 2.1, 'eV')] bg_ri_model = DEFAULT_MODEL_DICT['band_gap_refractive_index_moss'] refractive_indices = [bg_ri_model.evaluate({"Eg": bg}).pop('refractive_index') for bg in band_gaps] self.quantities_canonical_symbol = {"band_gaps": band_gaps, "refractive_indices": refractive_indices} self.sq_canonical_sym_as_dicts_no_value = copy.deepcopy(self.sq_custom_sym_as_dicts) self.sq_canonical_sym_as_dicts_no_value['band_gaps'] = self.sq_canonical_sym_as_dicts_no_value.pop('A') self.sq_canonical_sym_as_dicts_no_value['refractive_indices'] = self.sq_canonical_sym_as_dicts_no_value.pop('B') for d, sq in zip(self.sq_canonical_sym_as_dicts_no_value['band_gaps'], band_gaps): d.update({ "internal_id": sq._internal_id, "symbol_type": "band_gap", "units": "electron_volt", "provenance": ProvenanceStore.from_provenance_element(sq.provenance) }) d.pop('value') for d, sq in zip(self.sq_canonical_sym_as_dicts_no_value['refractive_indices'], refractive_indices): d.update({ "internal_id": sq._internal_id, "symbol_type": "refractive_index", "units": "dimensionless", "provenance": ProvenanceStore.from_provenance_element(sq.provenance) }) d.pop('value') self.sq_canonical_sym_values = {"band_gaps": [3.3, 2.1], "refractive_indices": [2.316340583741216, 2.593439239956374]} provenances_json['band_gaps'] = [ {'@module': 'propnet.dbtools.storage', '@class': 'ProvenanceStore', 'model': None, 'inputs': None, 'source': bg.provenance.source} for bg in band_gaps ] provenances_json['refractive_indices'] = [{ '@module': 'propnet.dbtools.storage', '@class': 'ProvenanceStore', 'model': 'band_gap_refractive_index_moss', 'inputs': [{'@module': 'propnet.dbtools.storage', '@class': 'ProvenanceStoreQuantity', 'data_type': 'NumQuantity', 'symbol_type': 'band_gap', 'internal_id': bg._internal_id, 'tags': [], 'provenance': pj}], 'source': ri.provenance.source} for bg, pj, ri in zip(band_gaps, provenances_json['band_gaps'], refractive_indices) ] self.sq_canonical_sym_json_no_value = copy.deepcopy(self.sq_canonical_sym_as_dicts_no_value) for sym in ["band_gaps", "refractive_indices"]: for q, p in zip(self.sq_canonical_sym_json_no_value[sym], provenances_json[sym]): q['provenance'] = p self.quantity_with_uncertainty = NumQuantity.from_weighted_mean(b) self.sq_with_uncertainty_as_dict_no_numbers = { '@module': 'propnet.dbtools.storage', '@class': 'StorageQuantity', 'internal_id': self.quantity_with_uncertainty._internal_id, 'data_type': 'NumQuantity', 'symbol_type': symbols['B'], 'units': 'dimensionless', 'provenance': ProvenanceStore.from_provenance_element( self.quantity_with_uncertainty.provenance), 'tags': []} provenances_json = { '@module': 'propnet.dbtools.storage', '@class': 'ProvenanceStore', 'model': 'aggregation', 'inputs': [ {'@module': 'propnet.dbtools.storage', '@class': 'ProvenanceStoreQuantity', 'data_type': 'NumQuantity', 'symbol_type': self.custom_symbols_json['B'], 'internal_id': b['internal_id'], 'tags': [], 'provenance': b['provenance']} for b in self.sq_custom_sym_json['B']], 'source': self.quantity_with_uncertainty.provenance.source } self.sq_with_uncertainty_json_no_numbers = copy.deepcopy(self.sq_with_uncertainty_as_dict_no_numbers) self.sq_with_uncertainty_json_no_numbers.update({"symbol_type": self.custom_symbols_json['B'], "provenance": provenances_json}) self.sq_with_uncertainty_numbers = {"value": 42.0, "uncertainty": 4.0} obj_symbol = symbols['C'] self.object_quantity = QuantityFactory.create_quantity(obj_symbol, "Test string") self.sq_object_as_dict = copy.deepcopy(self.sq_custom_sym_as_dicts['A'][0]) self.sq_object_as_dict.update({ "data_type": "ObjQuantity", "symbol_type": symbols['C'], "internal_id": self.object_quantity._internal_id, "value": "Test string", "units": None, "provenance": ProvenanceStore.from_provenance_element(self.object_quantity.provenance) }) self.sq_object_json = copy.deepcopy(self.sq_object_as_dict) self.sq_object_json.update( {"symbol_type": self.custom_syms_as_dicts['C'], "provenance": {'@module': 'propnet.dbtools.storage', '@class': 'ProvenanceStore', 'model': None, 'inputs': None, 'source': self.object_quantity.provenance.source}} ) # This setting allows dict differences to be shown in full self.maxDiff = None
def test_as_dict_from_dict(self): q = Quantity(self.custom_symbol, 5, tags='experimental', uncertainty=1) d = q.as_dict() d_storage = q.as_dict(for_storage=True) d_storage_omit = q.as_dict(for_storage=True, omit_value=True) self.assertEqual( d, { "@module": "propnet.core.quantity", "@class": "Quantity", "value": 5, "units": "dimensionless", "provenance": None, "symbol_type": self.custom_symbol.name }) self.assertEqual( d_storage, { "@module": "propnet.core.quantity", "@class": "Quantity", "value": 5, "units": "dimensionless", "provenance": None, "internal_id": q._internal_id, "symbol_type": self.custom_symbol.name }) self.assertEqual( d_storage_omit, { "@module": "propnet.core.quantity", "@class": "Quantity", "value": None, "units": None, "provenance": None, "internal_id": q._internal_id, "symbol_type": self.custom_symbol.name }) q = Quantity(self.custom_symbol, 5, tags='experimental', uncertainty=1, provenance=ProvenanceElement()) d = q.as_dict() d_storage = q.as_dict(for_storage=True) self.assertEqual( d, { "@module": "propnet.core.quantity", "@class": "Quantity", "value": 5, "units": "dimensionless", "provenance": q._provenance, "symbol_type": self.custom_symbol.name }) # Need more tests for provenance as_dict() method self.assertEqual( d_storage, { "@module": "propnet.core.quantity", "@class": "Quantity", "value": 5, "units": "dimensionless", "provenance": q._provenance.as_dict(), "internal_id": q._internal_id, "symbol_type": self.custom_symbol.name }) self.assertIsInstance(d_storage['provenance'], dict)
def process_item(self, item): # Define quantities corresponding to materials doc fields # Attach quantities to materials item = MontyDecoder().process_decoded(item) logger.info("Populating material for %s", item['task_id']) material = Material() if 'created_at' in item.keys(): date_created = item['created_at'] else: date_created = "" provenance = ProvenanceElement( source={ "source": self.source_name, "source_key": item['task_id'], "date_created": date_created }) for mkey, property_name in self.materials_symbol_map.items(): value = get(item, mkey) if value: material.add_quantity( QuantityFactory.create_quantity(property_name, value, provenance=provenance)) # Add custom things, e. g. computed entry computed_entry = get_entry(item) material.add_quantity( QuantityFactory.create_quantity("computed_entry", computed_entry, provenance=provenance)) material.add_quantity( QuantityFactory.create_quantity("external_identifier_mp", item['task_id'], provenance=provenance)) input_quantities = material.get_quantities() # Use graph to generate expanded quantity pool logger.info("Evaluating graph for %s", item['task_id']) graph = Graph() graph.remove_models({ "dimensionality_cheon": DEFAULT_MODEL_DICT['dimensionality_cheon'], "dimensionality_gorai": DEFAULT_MODEL_DICT['dimensionality_gorai'] }) new_material = graph.evaluate(material) # Format document and return logger.info("Creating doc for %s", item['task_id']) # Gives the initial inputs that were used to derive properties of a # certain material. doc = { "inputs": [StorageQuantity.from_quantity(q) for q in input_quantities] } for symbol, quantity in new_material.get_aggregated_quantities().items( ): all_qs = new_material._symbol_to_quantity[symbol] # Only add new quantities # TODO: Condition insufficiently general. # Can end up with initial quantities added as "new quantities" if len(all_qs) == 1 and list(all_qs)[0] in input_quantities: continue # Write out all quantities as dicts including the # internal ID for provenance tracing qs = [StorageQuantity.from_quantity(q).as_dict() for q in all_qs] # THE listing of all Quantities of a given symbol. sub_doc = { "quantities": qs, "mean": unumpy.nominal_values(quantity.value).tolist(), "std_dev": unumpy.std_devs(quantity.value).tolist(), "units": quantity.units.format_babel() if quantity.units else None, "title": quantity._symbol_type.display_names[0] } # Symbol Name -> Sub_Document, listing all Quantities of that type. doc[symbol.name] = sub_doc doc.update({ "task_id": item["task_id"], "pretty_formula": item["pretty_formula"] }) return jsanitize(doc, strict=True)
def evaluate(self, symbol_quantity_dict, allow_failure=True): """ Given a set of property_values, performs error checking to see if the corresponding input symbol_values represents a valid input set based on the self.connections() method. If so, returns a dictionary representing the value of plug_in applied to the input_symbols. The dictionary contains a "successful" key representing if plug_in was successful. The key distinction between evaluate and plug_in is properties in properties out vs. symbols in symbols out. In addition, evaluate also handles any requisite unit_mapping Args: symbol_quantity_dict ({property_name: Quantity}): a mapping of symbol names to quantities to be substituted allow_failure (bool): whether or not to catch errors in model evaluation Returns: dictionary of output properties with associated values generated from the input, along with "successful" if the substitution succeeds """ # Remap symbols and units if symbol map isn't none symbol_quantity_dict = self.map_properties_to_symbols( symbol_quantity_dict) # TODO: Is it really necessary to strip these? # TODO: maybe this only applies to pymodels or things with objects? # strip units from input and keep for reassignment symbol_value_dict = {} for symbol, quantity in symbol_quantity_dict.items(): # If unit map convert and then scrub units if self.unit_map.get(symbol): quantity = quantity.to(self.unit_map[symbol]) symbol_value_dict[symbol] = quantity.magnitude # Otherwise use values else: symbol_value_dict[symbol] = quantity.value # Plug in and check constraints try: out = self.plug_in(symbol_value_dict) except Exception as err: if allow_failure: return { "successful": False, "message": "{} evaluation failed: {}".format(self, err) } else: raise err if not self.check_constraints({**symbol_value_dict, **out}): return { "successful": False, "message": "Constraints not satisfied" } provenance = ProvenanceElement(model=self.name, inputs=list( symbol_quantity_dict.values())) out = self.map_symbols_to_properties(out) for symbol, value in out.items(): try: quantity = Quantity(symbol, value, self.unit_map.get(symbol), provenance=provenance) except SymbolConstraintError as err: if allow_failure: errmsg = "{} symbol constraint failed: {}".format( self, err) return {"successful": False, "message": errmsg} else: raise err if quantity.contains_nan_value(): return { "successful": False, "message": "Evaluation returned invalid values (NaN)" } out[symbol] = quantity out['successful'] = True return out
def evaluate(self, symbol_quantity_dict_in, allow_failure=True): """ Given a set of property_values, performs error checking to see if the corresponding input symbol_values represents a valid input set based on the self.connections() method. If so, returns a dictionary representing the value of plug_in applied to the input_symbols. The dictionary contains a "successful" key representing if plug_in was successful. The key distinction between evaluate and plug_in is properties in properties out vs. symbols in symbols out. In addition, evaluate also handles any requisite unit_mapping Args: symbol_quantity_dict ({property_name: Quantity}): a mapping of symbol names to quantities to be substituted allow_failure (bool): whether or not to catch errors in model evaluation Returns: dictionary of output properties with associated values generated from the input, along with "successful" if the substitution succeeds """ # Remap symbols and units if symbol map isn't none symbol_quantity_dict = self.map_properties_to_symbols( symbol_quantity_dict_in) input_symbol_quantity_dict = {k: v for k, v in symbol_quantity_dict.items() if not (k in self.constraint_symbols and k not in self.all_input_symbols)} for (k, v) in symbol_quantity_dict.items(): # replacing = self.symbol_property_map.get(k, k) replacing = self.symbol_property_map.get(k) # to_quantity() returns original object if it's already a BaseQuantity # unlike Quantity() which will return a deep copy symbol_quantity_dict[k] = QuantityFactory.to_quantity(replacing, v) # TODO: Is it really necessary to strip these? # TODO: maybe this only applies to pymodels or things with objects? # strip units from input and keep for reassignment symbol_value_dict = {} for symbol, quantity in symbol_quantity_dict.items(): # If unit map convert and then scrub units if self.unit_map.get(symbol): quantity = quantity.to(self.unit_map[symbol]) symbol_value_dict[symbol] = quantity.magnitude # Otherwise use values else: symbol_value_dict[symbol] = quantity.value contains_complex_input = any(NumQuantity.is_complex_type(v) for v in symbol_value_dict.values()) input_symbol_value_dict = {k: symbol_value_dict[k] for k in input_symbol_quantity_dict.keys()} # Plug in and check constraints try: with PrintToLogger(): out = self.plug_in(input_symbol_value_dict) except Exception as err: if allow_failure: return {"successful": False, "message": "{} evaluation failed: {}".format(self, err)} else: raise err if not self.check_constraints({**symbol_value_dict, **out}): return {"successful": False, "message": "Constraints not satisfied"} provenance = ProvenanceElement( model=self.name, inputs=list(input_symbol_quantity_dict.values()), source="propnet") out = self.map_symbols_to_properties(out) unit_map_as_properties = self.map_symbols_to_properties(self.unit_map) for symbol, value in out.items(): try: quantity = QuantityFactory.create_quantity( symbol, value, unit_map_as_properties.get(symbol), provenance=provenance) except SymbolConstraintError as err: if allow_failure: errmsg = "{} symbol constraint failed: {}".format(self, err) return {"successful": False, "message": errmsg} else: raise err if quantity.contains_nan_value(): return {"successful": False, "message": "Evaluation returned invalid values (NaN)"} # TODO: Update when we figure out how we're going to handle complex quantities # Model evaluation will fail if complex values are returned when no complex input was given # Can surely handle this more gracefully, or assume that the users will apply constraints if quantity.contains_imaginary_value() and not contains_complex_input: return {"successful": False, "message": "Evaluation returned invalid values (complex)"} out[symbol] = quantity out['successful'] = True return out