Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nicer model hashes; model_hash_to_model method #86

Merged
merged 13 commits into from
Oct 6, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions petab_select/constants.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Constants for the PEtab Select package."""
import string
import sys
from enum import Enum
from pathlib import Path
Expand Down Expand Up @@ -32,6 +33,14 @@
MODEL_CODE = 'model_code'
MODEL_HASH = 'model_hash'
MODEL_HASHES = 'model_hashes'
MODEL_SUBSPACE_INDICES_HASH_MAP = (
# [0-9]+[A-Z]+[a-z]
string.digits
+ string.ascii_uppercase
+ string.ascii_lowercase
)
MODEL_HASH_DELIMITER = '-'
HASHED_MODEL_SUBSPACE_INDICES_DELIMITER = '.'
# If `predecessor_model_hash` is defined for a model, it is the ID of the model that the
# current model was/is to be compared to. This is part of the result and is
# only (optionally) set by the PEtab calibration tool. It is not defined by the
Expand Down
107 changes: 71 additions & 36 deletions petab_select/model.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""The `Model` class."""
import string
import warnings
from os.path import relpath
from pathlib import Path
Expand All @@ -12,10 +13,13 @@
from .constants import (
CRITERIA,
ESTIMATED_PARAMETERS,
HASHED_MODEL_SUBSPACE_INDICES_DELIMITER,
MODEL_HASH,
MODEL_HASH_DELIMITER,
MODEL_ID,
MODEL_SUBSPACE_ID,
MODEL_SUBSPACE_INDICES,
MODEL_SUBSPACE_INDICES_HASH_MAP,
PARAMETERS,
PETAB_ESTIMATE_TRUE,
PETAB_PROBLEM,
Expand Down Expand Up @@ -58,14 +62,6 @@ class Model(PetabMixin):
Functions to convert attributes from :class:`Model` to YAML.
criteria:
The criteria values of the calibrated model (e.g. AIC).
hash_attributes:
This attribute is currently not used.
Attributes that will be used to calculate the hash of the
:class:`Model` instance. NB: this hash is used during pairwise comparison
to determine whether any two :class:`Model` instances are unique. The
model instances are compared by their parameter estimation
problems, as opposed to parameter estimation results, which may
differ due to e.g. floating-point arithmetic.
model_id:
The model ID.
petab_yaml:
Expand Down Expand Up @@ -130,28 +126,6 @@ class Model(PetabMixin):
for criterion_id, criterion_value in x.items()
},
}
hash_attributes = {
# MODEL_ID: lambda x: hash(x), # possible circular dependency on hash
# MODEL_SUBSPACE_ID: lambda x: hash(x),
# MODEL_SUBSPACE_INDICES: hash_list,
# TODO replace `YAML` with `PETAB_PROBLEM_HASH`, as YAML could refer to
# different problems if used on different filesystems or sometimes
# absolute and other times relative. Better to check whether the
# PEtab problem itself is unique.
# TODO replace `PARAMETERS` with `PARAMETERS_ALL`, which should be al
# parameters in the PEtab problem. This avoids treating the PEtab problem
# differently to the model (in a subspace with the PEtab problem) that has
# all nominal values defined in the subspace.
# TODO add `estimated_parameters`? Needs to be clarified whether this hash
# should be unique amongst only not-yet-calibrated models, or may also
# return the same value between differently parameterized models that ended
# up being calibrated to be the same... probably should be the former.
# Currently, the hash is stored, hence will "persist" after calibration
# if the same `Model` instance is used.
# PETAB_YAML: lambda x: hash(x),
PETAB_YAML: hash_str,
PARAMETERS: hash_parameter_dict,
}

def __init__(
self,
Expand Down Expand Up @@ -523,12 +497,7 @@ def get_hash(self) -> int:
The hash.
"""
if self.model_hash is None:
self.model_hash = hash_list(
[
method(getattr(self, attribute))
for attribute, method in Model.hash_attributes.items()
]
)
self.model_hash = hash_model(model=self)
return self.model_hash

def __hash__(self) -> None:
Expand Down Expand Up @@ -772,3 +741,69 @@ def models_to_yaml_list(
model_dicts = None if not model_dicts else model_dicts
with open(output_yaml, 'w') as f:
yaml.dump(model_dicts, f)


def unhash_model(model_hash: str):
"""Convert a model hash into model subspace information.

Args:
model_hash:
The model hash, in the format produced by :func:`hash_model`.

Returns:
The model subspace ID, and the indices that correspond to a unique
model in the subspace. The indices can be converted to a model with
the `ModelSubspace.indices_to_model` method.
"""
model_subspace_id, hashed_model_subspace_indices = model_hash.split(
MODEL_HASH_DELIMITER
)

if (
HASHED_MODEL_SUBSPACE_INDICES_DELIMITER
in hashed_model_subspace_indices
):
model_subspace_indices = [
int(s)
for s in hashed_model_subspace_indices.split(
HASHED_MODEL_SUBSPACE_INDICES_DELIMITER
)
]
else:
model_subspace_indices = [
MODEL_SUBSPACE_INDICES_HASH_MAP.index(s)
for s in hashed_model_subspace_indices
]

return model_subspace_id, model_subspace_indices


def hash_model(model: Model) -> str:
"""Create a unique hash for a model.

Args:
model:
The model.

Returns:
The hash. The format is the model subspace followed by a representation
of the indices of the model parameters in its subspace.
"""
try:
hashed_model_subspace_indices = ''.join(
MODEL_SUBSPACE_INDICES_HASH_MAP[index]
for index in model.model_subspace_indices
)
except KeyError:
hashed_model_subspace_indices = (
HASHED_MODEL_SUBSPACE_INDICES_DELIMITER.join(
str(i) for i in model.model_subspace_indices
)
)

model_hash = (
model.model_subspace_id
+ MODEL_HASH_DELIMITER
+ hashed_model_subspace_indices
)
return model_hash
21 changes: 20 additions & 1 deletion petab_select/problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
Criterion,
Method,
)
from .model import Model, default_compare
from .model import Model, default_compare, unhash_model
from .model_space import ModelSpace

__all__ = [
Expand Down Expand Up @@ -239,6 +239,25 @@ def get_best(
)
return best_model

def model_hash_to_model(self, model_hash: str) -> Model:
"""Get the model that matches a model hash.

Args:
model_hash:
The model hash, in the format produced by
:func:`petab_select.model.hash_model`.

Returns:
The model.
"""
model_subspace_id, model_subspace_indices = unhash_model(model_hash)
model = self.model_space.model_subspaces[
model_subspace_id
].indices_to_model(
indices=model_subspace_indices,
)
return model

def new_candidate_space(
self,
*args,
Expand Down
Loading