Source code for gssa.comparator.simulation_definition

# This file is part of the Go-Smart Simulation Architecture (GSSA).
# Go-Smart is an EU-FP7 project, funded by the European Commission.
#
# Copyright (C) 2013-  NUMA Engineering Ltd. (see AUTHORS file)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

from munkres import Munkres
import difflib
from .. import parameters

# CDM: Clinical Domain Model (see documentation)


[docs]class SimulationDefinition: """Abstract definition of a GSSA simulation. Ideally, this would be incorporated into GSSA itself as a nicer way of representing the GSSA-XML content for processing. It also contains an understanding of diffing. TODO: incorporate SimulationDefinition into server code This represents an argument to an algorithm (more generally, DB-defined lambda function) [see CDM] """
[docs] class Argument: name = "" def __init__(self, name): self.name = name # An argument is defined up to equivalence by its name
[docs] def diff(self, other): messages = [] if self.name != other.name: messages += ["Argument: names differ %s // %s" % (self.name, other.name)] return messages
def __eq__(self, other): return self.diff(other)
[docs] class Needle: """A percutaneous needle. More generally, one of a set of possible implements used in a procedure, possibly with repetition) [see CDM] """ index = "" cls = "" file = "" parameters = None def __init__(self, index, cls, file, parameters): self.index = index self.cls = cls self.file = file self.parameters = dict((p[0], SimulationDefinition.Parameter(*p)) for p in parameters)
[docs] def diff(self, other): """Needles are defined by their class, ID/file and their parameters (inc. location) [see CDM]""" messages = [] string_comparisons = { "cls": (self.cls, other.cls), "file": (self.file, other.file), } for field, pair in string_comparisons.items(): if pair[0] != pair[1]: messages += ["Needle: for index %s, %s fields differ %s // %s" % (self.index, field, pair[0], pair[1])] all_parameters = set().union(self.parameters.keys(), other.parameters.keys()) for name in all_parameters: if name not in self.parameters: messages += ["Needle: this (%s) has no parameter %s" % (self.index, name)] elif name not in other.parameters: messages += ["Needle: that (%s) has no parameter %s" % (other.index, name)] else: messages += self.parameters[name].diff(other.parameters[name]) return messages
def __eq__(self, other): return self.diff(other) == []
[docs] class Region: """Regions are geometric subdomains (2D/3D) [see CDM].""" id = "" name = "" format = "" input = "" groups = None def __init__(self, id, name, format, input, groups): self.id = id self.name = name self.format = format self.input = input self.groups = groups
[docs] def diff(self, other): """A region's definition is, strictly, in the separate geometry file describing it (usually), but the GSSA-XML should be able to provide enough information to tie it down.""" messages = [] string_comparisons = { "id": (self.id, other.id), "name": (self.name, other.name), "format": (self.format, other.format), "input": (self.input, other.input), } for field, pair in string_comparisons.items(): if pair[0] != pair[1]: messages += ["Region: for ID %s, %s fields differ %s // %s" % (self.id, field, pair[0], pair[1])] all_groups = set().union(self.groups, other.groups) for name in all_groups: if name not in self.groups: messages += ["Region: this (%s) has no group %s" % (self.id, name)] elif name not in other.groups: messages += ["Region: that (%s) has no group %s" % (other.id, name)] return messages
def __eq__(self, other): return self.diff(other) == []
[docs] class Algorithm: """An algorithm is a DB-defined lambda function that takes simulation-time arguments, such as Time or CurrentNeedleLength, and returns a Parameter-like value. In the GSSF case, these are generally MATC functions [see CDM].""" result = "" arguments = None content = "" def __init__(self, result, arguments, content): self.result = result self.arguments = dict((a, SimulationDefinition.Argument(a)) for a in arguments) self.content = content
[docs] def diff(self, other): """An Algorithm is defined by its result (parameter), arguments (above) and content (textual).""" messages = [] if self.result != other.result: messages += ["Algorithm: results differ %s // %s" % (self.result, other.result)] all_arguments = set().union(self.arguments.keys(), other.arguments.keys()) for name in all_arguments: if name not in self.arguments: messages += ["Algorithm: %s has no argument %s" % (self.result, name)] elif name not in other.arguments: messages += ["Algorithm: %s has no argument %s" % (other.result, name)] else: messages += self.arguments[name].diff(other.arguments[name]) if self.content != other.content: messages += ["Algorithm: %s content differs" % (self.result,)] return messages
def __eq__(self, other): return self.diff(other) == []
[docs] class NumericalModel: """The Numerical Model is a template or, say, a Python code using the helper Go-Smart module to define run-time GSSA parameters. The code is its definition, along with the regions, needles, parameters and so forth needed to complete it [see CDM].""" definition = "" regions = None needles = None def __init__(self, definition, regions, needles): self.definition = definition self.regions = dict((r[0], SimulationDefinition.Region(*r)) for r in regions) self.needles = dict((n[0], SimulationDefinition.Needle(*n)) for n in needles)
[docs] def diff(self, other): messages = [] # Note that this can only effectively compare embedded definitions if self.definition != other.definition: if not self.definition: messages += ["Numerical Model: this has no definition"] elif not other.definition: messages += ["Numerical Model: that has no definition"] else: d = difflib.unified_diff(self.definition.splitlines(), other.definition.splitlines()) messages += ["Numerical Model: definitions differ:\n | " + "\n | ".join(line.strip() for line in d)] all_regions = set().union(self.regions.keys(), other.regions.keys()) for id in all_regions: if id not in self.regions: messages += ["Numerical Model: this has no region %s" % id] elif id not in other.regions: messages += ["Numerical Model: that has no region %s" % id] else: messages += self.regions[id].diff(other.regions[id]) diff_matrix = [] this_keys = list(self.needles.keys()) that_keys = list(other.needles.keys()) if len(this_keys) != len(that_keys): messages += ["Numerical Model: this has different needle count than that"] if len(this_keys) > 0 and len(that_keys) > 0: for this_key in this_keys: diff_row = [] for that_key in that_keys: needle_messages = self.needles[this_key].diff(other.needles[that_key]) diff_row.append(len(needle_messages)) diff_matrix.append(diff_row) m = Munkres() indexes = m.compute(diff_matrix) for row, column in indexes: messages += self.needles[this_keys[row]].diff(other.needles[that_keys[column]]) return messages
def __eq__(self, other): return self.diff(other) == []
[docs] class Parameter: """This is the fundamental class representing an arbitrary-type attribute of a simulation [see CDM].""" value = None typ = "" name = "" def __init__(self, name, value, typ): self.name = name self.typ = typ self.value = parameters.convert_parameter(value, typ)
[docs] def diff(self, other): messages = [] if self.name != other.name: messages += ["Parameter: names differ - %s // %s" % (self.name, other.name)] else: if self.typ != other.typ: messages += ["Parameter %s: types differ - %s // %s" % (self.name, self.typ, other.typ)] if self.value != other.value: messages += ["Parameter %s: values differ - %s // %s" % (self.name, str(self.value), str(other.value))] return sorted(messages)
def __eq__(self, other): return self.diff(other) == []
[docs] class Transferrer: """This is not part of the CDM, being a setting indicating how the simulation server should receive or send separate files, however it is a key component of GSSA-XML.""" url = "" cls = "" def __init__(self, cls, url): self.url = url self.cls = cls def __eq__(self, other): return self.diff(other) == []
[docs] def diff(self, other): messages = [] if self.url != other.url: messages += ["Transferrer: URLs differ - %s // %s" % (self.url, other.url)] if self.cls != other.cls: messages += ["Transferrer: classes differ - %s // %s" % (self.cls, other.cls)] return sorted(messages)
transferrer = None parameters = None algorithms = None numerical_model = None name = "This" def __init__(self, name): self.parameters = {} self.algorithms = {} self.name = name
[docs] def add_parameter(self, name, value, typ): self.parameters[name] = self.Parameter(name, value, typ)
[docs] def add_algorithm(self, result, arguments, content): self.algorithms[result] = self.Algorithm(result, arguments, content)
[docs] def set_transferrer(self, cls, url): self.transferrer = self.Transferrer(cls, url)
[docs] def set_numerical_model(self, definition, regions, needles): self.numerical_model = self.NumericalModel(definition, regions, needles)
[docs] def diff(self, other): """Produce a series of human-readable messages describing the non-equivalences between this and another ("that") definition.""" messages = [] # At each step we check whether the relevant component is present in one # or both definitions, then request a diff for it if self.transferrer or other.transferrer: if not self.transferrer: messages += ["%s definition has no transferrer" % self.name] elif not other.transferrer: messages += ["%s other definition has no transferrer" % other.name] else: messages += self.transferrer.diff(other.transferrer) if self.algorithms or other.algorithms: if not self.algorithms: messages += ["%s definition has no algorithms" % other.name] elif not other.algorithms: messages += ["%s definition has no algorithms" % other.name] else: all_algorithms = set().union(self.algorithms.keys(), other.algorithms.keys()) for name in all_algorithms: if name not in self.algorithms: messages += ["%s definition has no algorithm '%s'" % (self.name, name)] elif name not in other.algorithms: messages += ["%s definition has no algorithm '%s'" % (other.name, name)] else: messages += self.algorithms[name].diff(other.algorithms[name]) if self.parameters or other.parameters: if not self.parameters: messages += ["%s definition has no parameters" % self.name] elif not other.parameters: messages += ["%s definition has no parameters" % other.name] else: # For comparing parameters, we first check the keys match, then # compare type/value-wise all_parameters = set().union(self.parameters.keys(), other.parameters.keys()) for name in all_parameters: if name not in self.parameters: messages += ["%s definition has no parameter '%s'" % (self.name, name)] elif name not in other.parameters: messages += ["%s definition has no parameter '%s'" % (other.name, name)] else: messages += self.parameters[name].diff(other.parameters[name]) if self.numerical_model or other.numerical_model: if not self.numerical_model: messages += ["%s definition has no numerical model" % self.name] elif not other.numerical_model: messages += ["%s definition has no numerical model" % other.name] else: messages += self.numerical_model.diff(other.numerical_model) # Messages are sorted for readability return sorted(messages)
def __eq__(self, other): return self.diff(other) == []