aspect-data 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
aspect/io.py ADDED
@@ -0,0 +1,189 @@
1
+
2
+ from typing import TYPE_CHECKING, Any, Iterable, Mapping, Optional, Union
3
+ from functools import partial
4
+ import hashlib
5
+ import os
6
+ import tempfile
7
+
8
+ from carabiner import print_err
9
+
10
+ from numpy.typing import ArrayLike
11
+
12
+ if TYPE_CHECKING:
13
+ from datasets import Dataset, IterableDataset
14
+ from pandas import DataFrame
15
+ else:
16
+ Dataset, DataFrame, IterableDataset = Any, Any, Any
17
+
18
+ from .package_data import DEFAULT_CACHE
19
+
20
+
21
+ DATASETS_PREFIX: str = "hf://datasets/"
22
+
23
+ def hasher(s: str, n: int = 16) -> str:
24
+ if isinstance(s, str):
25
+ s = s.encode()
26
+ return hashlib.sha256(s).hexdigest()[:n]
27
+
28
+ def _lock_path(cache_dir: str, key: str) -> str:
29
+ locks_dir = os.path.join(cache_dir, ".locks")
30
+ os.makedirs(locks_dir, exist_ok=True)
31
+ h = hasher(key.encode("utf-8"))
32
+ return os.path.join(locks_dir, f"{h}.lock")
33
+
34
+
35
+ def _load_from_file(filename: str, cache: Optional[str] = None) -> Dataset:
36
+ from datasets import load_dataset, Dataset, DatasetDict
37
+ from filelock import FileLock
38
+
39
+ cache = cache or DEFAULT_CACHE
40
+ if filename.removesuffix(".gz").endswith((".csv", ".tsv", ".txt")):
41
+ read_f = partial(
42
+ load_dataset,
43
+ path="csv",
44
+ data_files=filename,
45
+ cache_dir=cache,
46
+ sep="," if filename.endswith((".csv", ".csv.gz")) else "\t",
47
+ )
48
+ lock_key = ("csv", read_f.keywords.get('sep'))
49
+ elif filename.endswith((".arrow", ".hd5", ".json", ".parquet", ".xml")):
50
+ _, ext = os.path.splitext(filename)
51
+ protocol = ext.lstrip(".")
52
+ read_f = partial(
53
+ load_dataset,
54
+ path=protocol,
55
+ data_files=filename,
56
+ cache_dir=cache,
57
+ )
58
+ lock_key = (protocol, "")
59
+ elif filename.endswith(".hf"):
60
+ read_f = partial(
61
+ Dataset.load_from_disk,
62
+ dataset_path=filename,
63
+ )
64
+ lock_key = ("hf", "")
65
+ else:
66
+ raise IOError(f"Could not infer how to open '{filename}' from its extension.")
67
+
68
+ # If no cache, nothing sensible to lock on
69
+ if cache is None:
70
+ ds = read_f()
71
+
72
+ # Cross-task lock on the shared filesystem
73
+ lockfile = _lock_path(cache, "_".join(lock_key))
74
+ with FileLock(lockfile, timeout=60. * 60.):
75
+ ds = read_f()
76
+ if isinstance(ds, DatasetDict):
77
+ return ds["train"]
78
+ else:
79
+ return ds
80
+
81
+
82
+ def _load_from_dataframe(
83
+ dataframe: Union[DataFrame, Mapping[str, ArrayLike], Iterable[Mapping[str, ArrayLike]]],
84
+ cache: Optional[str] = None
85
+ ) -> Dataset:
86
+ from pandas import DataFrame
87
+
88
+ if cache is None:
89
+ cache = DEFAULT_CACHE
90
+ print_err(f"Defaulting to cache: {cache}")
91
+ if not isinstance(dataframe, DataFrame):
92
+ dataframe = DataFrame(dataframe)
93
+
94
+ hash_name = hasher(dataframe.to_string())
95
+ with tempfile.TemporaryDirectory() as tmpdir:
96
+ csv_filename = os.path.join(tmpdir, f"{hash_name}.csv.gz")
97
+ dataframe.to_csv(csv_filename, index=False)
98
+ ds = _load_from_file(
99
+ csv_filename,
100
+ cache=cache,
101
+ )
102
+ return ds
103
+
104
+
105
+ def _get_ref_chunk(
106
+ s,
107
+ sep: Optional[str] = None,
108
+ all_seps: str = "@~:"
109
+ ) -> str:
110
+ if sep is not None:
111
+ if sep in s:
112
+ s = s.rpartition(sep)[-1]
113
+ else:
114
+ return None
115
+ for _sep in all_seps:
116
+ s = s.partition(_sep)[0]
117
+ return s
118
+
119
+
120
+ def _resolve_hf_hub_dataset(
121
+ ref: str,
122
+ cache: Optional[str] = None
123
+ ) -> Dataset:
124
+ from datasets import concatenate_datasets, load_dataset, DatasetDict
125
+
126
+ ref = ref.removeprefix(DATASETS_PREFIX).removeprefix("hf://")
127
+ seps = "@~:"
128
+ ver = _get_ref_chunk(ref, "@", all_seps=seps)
129
+ split = _get_ref_chunk(ref, ":", all_seps=seps)
130
+ config = _get_ref_chunk(ref, "~", all_seps=seps)
131
+
132
+ ds = load_dataset(
133
+ path=_get_ref_chunk(ref, all_seps=seps),
134
+ name=config,
135
+ split=split,
136
+ revision=ver,
137
+ cache_dir=cache,
138
+ )
139
+ if isinstance(ds, DatasetDict):
140
+ ds = concatenate_datasets([v for key, v in ds.items()])
141
+ return ds
142
+
143
+
144
+ class AutoDataset:
145
+
146
+ def __init__(self, dataset):
147
+ self._dataset = dataset
148
+
149
+ @classmethod
150
+ def load(
151
+ cls,
152
+ data: Union[str, DataFrame],
153
+ cache: Optional[str] = None
154
+ ) -> Union[Dataset, IterableDataset]:
155
+ from datasets import load_dataset, Dataset, IterableDataset
156
+ from pandas import DataFrame
157
+
158
+ if isinstance(data, (Dataset, IterableDataset)):
159
+ dataset = data
160
+ elif isinstance(data, (DataFrame, Mapping)):
161
+ dataset = _load_from_dataframe(
162
+ data,
163
+ cache=cache,
164
+ )
165
+ elif isinstance(data, str):
166
+ if data.startswith("hf://"):
167
+ dataset = _resolve_hf_hub_dataset(
168
+ data,
169
+ cache=cache,
170
+ )
171
+ elif os.path.exists(data):
172
+ dataset = _load_from_file(
173
+ data,
174
+ cache=cache,
175
+ )
176
+ else:
177
+ raise ValueError(
178
+ f"""
179
+ If `data` is a string, it must start with "{DATASETS_PREFIX}" or a path to an existing file.
180
+ It was "{data}".
181
+ """
182
+ )
183
+ else:
184
+ raise ValueError(
185
+ """
186
+ Data must be a string, Dataset, dictionary, or Pandas DataFrame.
187
+ """
188
+ )
189
+ return cls(dataset)
aspect/package_data.py ADDED
@@ -0,0 +1,35 @@
1
+ """Tools for loading and writing reusable package data."""
2
+
3
+ import os
4
+
5
+ from platformdirs import user_cache_dir
6
+
7
+ from . import app_name, __author__, __version__
8
+
9
+ CACHE_DIR = user_cache_dir(
10
+ app_name,
11
+ appauthor=False,
12
+ version=__version__,
13
+ )
14
+
15
+ ELUENT_CACHE = "ASPECT_CACHE"
16
+ DEFAULT_CACHE = CACHE_DIR
17
+
18
+ def _get_data_path(
19
+ filename: str,
20
+ env_key: str = ELUENT_CACHE,
21
+ default: str = DEFAULT_CACHE
22
+ ) -> str:
23
+ """Returns the path to a writable version of a package data file.
24
+
25
+ Copies it from the package resources if not present.
26
+
27
+ """
28
+ cache_dir = os.environ.get(
29
+ env_key,
30
+ os.path.expanduser(default),
31
+ )
32
+ os.makedirs(cache_dir, exist_ok=True)
33
+ os.environ["HF_HOME"] = cache_dir
34
+ os.environ["HF_DATASETS_CACHE"] = cache_dir
35
+ return cache_dir, os.path.join(cache_dir, filename)
@@ -0,0 +1,9 @@
1
+ from .registry import FUNCTION_REGISTRY
2
+
3
+ def _load_all():
4
+ try:
5
+ from . import functions, deep_functions # importing populates FUNCTION_REGISTRY as a side effect
6
+ except (ImportError, NameError):
7
+ pass
8
+
9
+ _load_all()
@@ -0,0 +1,182 @@
1
+ """Classes to enable JSON serialization of preprocessing functions."""
2
+
3
+ from typing import Any, Callable, Dict, Iterable, Mapping, Union
4
+ from dataclasses import asdict, dataclass, field
5
+ from functools import partial
6
+
7
+ from carabiner import print_err
8
+ import numpy as np
9
+
10
+ from .. import app_name, __version__
11
+ from ..checkpoint_utils import save_json, _load_json
12
+ from .registry import FUNCTION_REGISTRY
13
+
14
+
15
+ @dataclass
16
+ class ColumnTransform:
17
+
18
+ """Serializable wrapper around a registered transform function.
19
+
20
+ Examples
21
+ ========
22
+ Basic construction and output column naming:
23
+
24
+ >>> ct = ColumnTransform(name="log", input_column="affinity")
25
+ >>> ct.input_column
26
+ 'affinity'
27
+ >>> ct.name
28
+ 'log'
29
+ >>> ct.output_column.startswith("aspect-data/")
30
+ True
31
+
32
+ Output column is deterministic:
33
+
34
+ >>> ct2 = ColumnTransform(name="log", input_column="affinity")
35
+ >>> ct.output_column == ct2.output_column
36
+ True
37
+
38
+ Output column changes with input_column:
39
+
40
+ >>> ct3 = ColumnTransform(name="log", input_column="other")
41
+ >>> ct.output_column != ct3.output_column
42
+ True
43
+
44
+ Output column changes with kwargs:
45
+
46
+ >>> ct4 = ColumnTransform(name="hash", input_column="assay", kwargs={"ndim": 64})
47
+ >>> ct5 = ColumnTransform(name="hash", input_column="assay", kwargs={"ndim": 128})
48
+ >>> ct4.output_column != ct5.output_column
49
+ True
50
+
51
+ Applying a transform:
52
+
53
+ >>> import numpy as np
54
+ >>> data = {"affinity": [1.0, 10.0, 100.0]}
55
+ >>> result = ct(data)
56
+ >>> ct.output_column in result
57
+ True
58
+ >>> np.allclose(result[ct.output_column], np.log([1., 10., 100.])[:,None])
59
+ True
60
+
61
+ Idempotency — calling twice skips recomputation:
62
+
63
+ >>> result2 = ct(result)
64
+ >>> np.array_equal(result[ct.output_column], result2[ct.output_column])
65
+ True
66
+
67
+ Serialization roundtrip:
68
+
69
+ >>> d = ct.to_dict()
70
+ >>> ct_restored = ColumnTransform.from_dict(d)
71
+ >>> ct_restored.output_column == ct.output_column
72
+ True
73
+ >>> data2 = {"affinity": [1.0, 10.0, 100.0]}
74
+ >>> result3 = ct_restored(data2)
75
+ >>> np.allclose(result3[ct.output_column], np.log([1., 10., 100.])[:,None])
76
+ True
77
+
78
+ Chained application propagates columns correctly:
79
+
80
+ >>> ct_hash = ColumnTransform(name="hash", input_column="assay", kwargs={"ndim": 16})
81
+ >>> data3 = {"assay": ["MIC", "MIC", "binding"]}
82
+ >>> r1 = ct_hash(data3)
83
+ >>> ct_hash.output_column in r1
84
+ True
85
+ >>> r1[ct_hash.output_column].shape
86
+ (3, 16)
87
+
88
+ Unregistered name raises:
89
+
90
+ >>> ColumnTransform(name="nonexistent", input_column="x")
91
+ Traceback (most recent call last):
92
+ ...
93
+ ValueError: Function 'nonexistent' is not registered...
94
+
95
+ show() lists registered functions:
96
+
97
+ >>> transforms = ColumnTransform.show()
98
+ >>> "log" in transforms
99
+ True
100
+ >>> "hash" in transforms
101
+ True
102
+
103
+ """
104
+
105
+ name: Union[str, Callable]
106
+ input_column: str
107
+ kwargs: Mapping[str, Any] = field(default_factory = dict)
108
+ _version: str = field(default=__version__)
109
+ _app: str = field(default=app_name)
110
+
111
+ def __post_init__(self):
112
+ if isinstance(self.name, str):
113
+ try:
114
+ self.closure = FUNCTION_REGISTRY[self.name]
115
+ except KeyError:
116
+ raise ValueError(
117
+ f"Function '{self.name}' is not registered. Try: {', '.join(FUNCTION_REGISTRY)}"
118
+ )
119
+ self._serializable = True
120
+ elif isinstance(self.name, Callable):
121
+ self.closure = self.name
122
+ self.name = str(self.name)
123
+ self._serializable = False
124
+ else:
125
+ raise ValueError(f"Transformation function must be a str or callable")
126
+ self.hash = self._get_hash()
127
+ self.hash_stub = self.hash[:8]
128
+ self.output_column = self._get_output_column()
129
+ self.function = self.closure(**self.kwargs)
130
+
131
+ def _get_hash(self):
132
+ from datasets.fingerprint import Hasher
133
+ return Hasher.hash(self.to_dict())
134
+
135
+ def _get_output_column(self):
136
+ return f"{self._app}/{self._version}/{self.name}:{self.input_column}/{self.hash_stub}"
137
+
138
+ def to_dict(self) -> Dict[str, Any]:
139
+ """Convert the function call to a JSON-serializable format."""
140
+ if self._serializable:
141
+ return asdict(self)
142
+ else:
143
+ raise AttributeError(f"Cannot serialize {self} with callable {self.name}.")
144
+
145
+ def to_file(self, filename: str) -> None:
146
+ """Save to JSON."""
147
+ return save_json(self.to_dict(), filename)
148
+
149
+ @classmethod
150
+ def from_dict(cls, data: Mapping[str, Union[str, Mapping]]) -> 'ColumnTransform':
151
+ """Reconstruct the function call from JSON."""
152
+ return cls(**data)
153
+
154
+ @classmethod
155
+ def from_file(cls, filename: str) -> 'ColumnTransform':
156
+ """Load from JSON."""
157
+ return cls.from_dict(_load_json(filename))
158
+
159
+ @classmethod
160
+ def show(cls) -> 'ColumnTransform':
161
+ """List registered functions."""
162
+ return tuple(FUNCTION_REGISTRY)
163
+
164
+ def apply(
165
+ self,
166
+ inputs: Mapping[str, Iterable]
167
+ ) -> np.ndarray:
168
+ result = self.function(inputs, self.input_column)
169
+ if result.ndim == 1:
170
+ result = result[:, None]
171
+ return result
172
+
173
+ def __call__(
174
+ self,
175
+ inputs: Mapping[str, Iterable]
176
+ ) -> Dict[str, np.ndarray]:
177
+ """Execute the function with stored parameters."""
178
+ if self.output_column not in inputs:
179
+ inputs[self.output_column] = self.apply(inputs)
180
+ else:
181
+ print_err(f"[WARN] {self.output_column} already present, skipping.")
182
+ return inputs
@@ -0,0 +1,143 @@
1
+ """Preprocessing functions from deep embeddings."""
2
+
3
+ from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, Mapping, Tuple
4
+ from functools import partial
5
+
6
+ from carabiner import cast, print_err
7
+ import numpy as np
8
+ from numpy.typing import ArrayLike
9
+
10
+ if TYPE_CHECKING:
11
+ from transformers import PreTrainedTokenizerBase, PreTrainedModel
12
+ else:
13
+ PreTrainedTokenizerBase, PreTrainedModel = Any, Any
14
+
15
+ from .registry import register_function
16
+ from ..typing import StrOrIterableOfStr
17
+
18
+ def get_device():
19
+ import torch
20
+ return torch.device("cuda" if torch.cuda.is_available() else "cpu")
21
+
22
+
23
+ def _index_into(x: ArrayLike, i: int):
24
+ return x[:,i,:]
25
+
26
+
27
+ def _get_value_from_tuple(f: Callable, **kwargs):
28
+ partial_f = partial(f, **kwargs)
29
+
30
+ def _f(*args, **fkwargs):
31
+ return partial_f(*args, **fkwargs).values
32
+
33
+ return _f
34
+
35
+
36
+ def _resolve_bart_model(ref: str) -> Tuple[PreTrainedTokenizerBase, PreTrainedModel]:
37
+ from transformers import AutoModelForSeq2SeqLM, AutoTokenizer
38
+ import torch
39
+ model = AutoModelForSeq2SeqLM.from_pretrained(ref)
40
+ return AutoTokenizer.from_pretrained(ref), torch.compile(model, fullgraph=True).to(get_device())
41
+
42
+
43
+ def _tokenize_for_embedding(
44
+ x: Iterable[str],
45
+ tokenizer: PreTrainedTokenizerBase
46
+ ) -> Dict[str, np.ndarray]:
47
+
48
+ inputs = [
49
+ _x if _x is not None
50
+ else '<unk>'
51
+ for _x in cast(x, to=list)
52
+ ]
53
+ tokenizer_args = {
54
+ "return_tensors": "pt",
55
+ "padding": True,
56
+ }
57
+ try:
58
+ tokenized = tokenizer(inputs, **tokenizer_args)
59
+ except (ValueError, TypeError) as e:
60
+ print_err(e)
61
+ print_err(f"Tokenizing this batch failed: {inputs}")
62
+ tokenized = tokenizer(
63
+ ['<unk>'] * len(inputs),
64
+ **tokenizer_args,
65
+ )
66
+
67
+ return {
68
+ key: tokenized[key].to(get_device())
69
+ for key in ['input_ids', 'attention_mask']
70
+ }
71
+
72
+
73
+ @register_function("hf-bart")
74
+ def HfBART(
75
+ ref: str,
76
+ aggregator: StrOrIterableOfStr = "mean"
77
+ ) -> np.ndarray:
78
+
79
+ try:
80
+ from transformers import PreTrainedTokenizerBase, PreTrainedModel
81
+ except ImportError:
82
+ raise ImportError("Hugging Face transformers not installed. Try `pip install aspect[deep]`.")
83
+ else:
84
+ import torch
85
+
86
+ EMBEDDING_AGGREGATORS = {
87
+ "start": partial(_index_into, i=0),
88
+ "end": partial(_index_into, i=-1),
89
+ "sum": partial(torch.sum, dim=-2),
90
+ "mean": partial(torch.mean, dim=-2),
91
+ "median": _get_value_from_tuple(torch.median, dim=-2),
92
+ "max": _get_value_from_tuple(torch.max, dim=-2),
93
+ }
94
+ DEFAULT_AGGREGATOR = EMBEDDING_AGGREGATORS["mean"]
95
+
96
+ def _aggregated_embedding(
97
+ x: torch.Tensor,
98
+ aggregators: Iterable[Callable] = (DEFAULT_AGGREGATOR, )
99
+ ):
100
+ aggregated = [
101
+ a(x).detach().cpu().numpy() for a in aggregators
102
+ ]
103
+
104
+ return np.concatenate(aggregated, axis=-1)
105
+
106
+
107
+ aggregators = [a.casefold() for a in cast(aggregator, to=list)]
108
+ incorrect_agg = [
109
+ a for a in aggregators if a not in EMBEDDING_AGGREGATORS
110
+ ]
111
+ if len(incorrect_agg) > 0:
112
+ raise ValueError(f"Requested aggregators do not exist: {incorrect_agg}")
113
+ else:
114
+ aggregators = [EMBEDDING_AGGREGATORS[a] for a in aggregators]
115
+ tokenizer, model = _resolve_bart_model(ref)
116
+
117
+ def _hf_bart(
118
+ data: Mapping[str, Iterable],
119
+ input_column: str
120
+ ) -> np.ndarray:
121
+ tokenized_inputs = _tokenize_for_embedding(
122
+ data[input_column],
123
+ tokenizer=tokenizer,
124
+ )
125
+ model.eval()
126
+ with torch.no_grad():
127
+ outputs = model(
128
+ **tokenized_inputs,
129
+ decoder_input_ids=tokenized_inputs['input_ids'],
130
+ output_hidden_states=True,
131
+ return_dict=True,
132
+ )
133
+ enc_last = _aggregated_embedding(
134
+ outputs.encoder_hidden_states[-1],
135
+ aggregators=aggregators,
136
+ )
137
+ dec_last = _aggregated_embedding(
138
+ outputs.decoder_hidden_states[-1],
139
+ aggregators=aggregators,
140
+ )
141
+ return np.concatenate([enc_last, dec_last], axis=-1)
142
+
143
+ return _hf_bart