nucleobench 0.1.0__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.
File without changes
@@ -0,0 +1,64 @@
1
+ """Utilities for parsing arguments."""
2
+
3
+ from typing import Iterable
4
+
5
+ import argparse
6
+ import dataclasses
7
+
8
+ @dataclasses.dataclass
9
+ class ParsedArgs:
10
+ main_args: argparse.Namespace
11
+ model_init_args: argparse.Namespace
12
+ opt_init_args: argparse.Namespace
13
+ opt_run_args: argparse.Namespace
14
+
15
+
16
+ def parse_long_start_sequence(known_args: argparse.Namespace) -> argparse.Namespace:
17
+ """Parse a long start sequence from a file."""
18
+ assert known_args.seed_sequence.startswith('local://')
19
+ local_fileloc = known_args.seed_sequence[len('local://'):]
20
+ with open(local_fileloc, 'r') as f:
21
+ known_args.seed_sequence = f.read()
22
+ return known_args
23
+
24
+
25
+ def possibly_parse_positions_to_mutate(known_args: argparse.Namespace) -> argparse.Namespace:
26
+ """Possibly parse `positions_to_mutate` from a file, or leave it untouched, depending on the value."""
27
+ if isinstance(known_args.positions_to_mutate, str) and known_args.positions_to_mutate.startswith('local://'):
28
+ local_fileloc = known_args.positions_to_mutate[len('local://'):]
29
+ with open(local_fileloc, 'r') as f:
30
+ loc_str = f.read()
31
+ known_args.positions_to_mutate = [int(x) for x in loc_str.split('\n')]
32
+ elif known_args.positions_to_mutate is None or known_args.positions_to_mutate == '' or known_args.positions_to_mutate == []:
33
+ known_args.positions_to_mutate = None
34
+ else:
35
+ assert isinstance(known_args.positions_to_mutate, list), (type(known_args.positions_to_mutate), known_args.positions_to_mutate)
36
+ known_args.positions_to_mutate = [int(x) for x in known_args.positions_to_mutate.split(',')]
37
+ return known_args
38
+
39
+
40
+ def handle_leftover_args(known_args: argparse.Namespace, leftover_args: Iterable):
41
+ """Handle leftover arguments, either by failing or by ignoring them."""
42
+ if known_args.ignore_empty_cmd_args:
43
+ # Check that every "value" is either `None` or `empty`. If so, allow it to continue.
44
+ for i in leftover_args:
45
+ if i.startswith('--'):
46
+ if '=' in i:
47
+ arg_val = i.split('=')[1]
48
+ if arg_val not in [None, '']:
49
+ raise ValueError(f'Unused arg, not empty: {leftover_args}')
50
+ continue
51
+ else:
52
+ if i not in [None, '']:
53
+ raise ValueError(f'Unused arg, not empty: {leftover_args}')
54
+ else:
55
+ raise ValueError(f'Unused args: {leftover_args}')
56
+
57
+
58
+ def str_to_bool(s):
59
+ if s.lower() in ('yes', 'true', 't', '1'):
60
+ return True
61
+ elif s.lower() in ('no', 'false', 'f', '0'):
62
+ return False
63
+ else:
64
+ raise argparse.ArgumentTypeError('Boolean value expected.')
@@ -0,0 +1,199 @@
1
+ """Library for smoothgrad and genome-specific attribution methods.
2
+
3
+ Ref:
4
+ 1. [Correcting gradient-based interpretations of deep neural networks for genomics](https://genomebiology.biomedcentral.com/articles/10.1186/s13059-023-02956-3)
5
+ 2. [SmoothGrad: removing noise by adding noise](https://arxiv.org/abs/1706.03825)
6
+ 3. [Quick and effective approximation of in silico saturation mutagenesis experiments with first-order taylor expansion](https://pubmed.ncbi.nlm.nih.gov/39286491/)
7
+
8
+ TODO(joelshor): Consider using public version of attribution tools, such as:
9
+ - PyTorch Smoothgrad: https://github.com/pkmr06/pytorch-smoothgrad
10
+ - PyTorch GradCam and others: https://github.com/jacobgil/pytorch-grad-cam?tab=readme-ov-file
11
+ - PyTorch Smoothgrad and others: https://tf-explain.readthedocs.io/en/latest/
12
+
13
+ To test locally:
14
+ ```zsh
15
+ python -m nucleobench.common.attribution_lib
16
+ ```
17
+ """
18
+
19
+ import gc
20
+ import numpy as np
21
+ import torch
22
+ from typing import Callable, Optional
23
+
24
+
25
+ TISMOutputType = list[dict[str, float]]
26
+ SmoothgradVocabType = list[dict[str, torch.Tensor]]
27
+ TISMLocationsType = list[int]
28
+
29
+
30
+ def noise_inputs(
31
+ input_tensor: torch.Tensor,
32
+ noise_stdev: float,
33
+ times: int,
34
+ ) -> torch.Tensor:
35
+ """Generates noisy inputs.
36
+
37
+ NOTE: For simplicity, for now, we work with SINGLE TENSORS. Assume no batch dimension.
38
+
39
+ Args:
40
+ input_tensor: Input tensor. Doesn't have to be genomic. Should NOT be batched.
41
+ noise_stdev: Noise to add.
42
+ times: Number of times to add noise.
43
+ """
44
+ if noise_stdev < 0:
45
+ raise ValueError(f'Requires non-negative noise stdev: {noise_stdev}')
46
+ x = input_tensor # Syntactic sugar.
47
+
48
+ # Stack N versions of the input, to add uncorrelated noise to.
49
+ with torch.no_grad():
50
+ x = x.unsqueeze(0)
51
+ x = x.repeat([times] + [1] * (x.ndim-1))
52
+
53
+ # Add noise for hte smoothgrad algorithm.
54
+ if noise_stdev > 0:
55
+ noise_to_add = torch.normal(mean=torch.zeros(x.shape), std=noise_stdev)
56
+ x += noise_to_add
57
+ return x
58
+
59
+ def noisy_grads_torch(
60
+ input_tensor: torch.Tensor,
61
+ model: Callable[[torch.Tensor], torch.Tensor],
62
+ noise_stdev: float,
63
+ times: int,
64
+ idxs: Optional[TISMLocationsType] = None,
65
+ ) -> torch.Tensor:
66
+ """Generates noisy gradients from a function.
67
+
68
+ NOTE: For simplicity, for now, we work with SINGLE TENSORS. Assume no batch dimension.
69
+
70
+ This replicates the input `times` times, and runs it through the network all at once.
71
+
72
+ TODO(joelshor): Add batching, for the situation where `times` is larger than the possible batch size
73
+ of a single inference with a network.
74
+ TODO(joelshor): Add ability to efficiently compute multiple inputs at once.
75
+
76
+ Args:
77
+ input_tensor (torch.Tensor): Input tensor. Doesn't have to be genomic. Should NOT be batched.
78
+ model: PyTorch model to use. The model must return a scalar per batch element.
79
+ noise_stdev: Noise to add.
80
+ times: Number of times to add noise.
81
+ idx: If present, only backprop through this location.
82
+ """
83
+ x = noise_inputs(
84
+ input_tensor=input_tensor,
85
+ noise_stdev=noise_stdev,
86
+ times=times)
87
+
88
+ # Run inference to get grads.
89
+ if idxs is None:
90
+ x_grad = x
91
+ x_grad.requires_grad = True
92
+ else:
93
+ x, x_grad = apply_gradient_mask(x, idxs)
94
+
95
+ y = model(x)
96
+ y_sum = y.sum()
97
+ y_sum.backward(retain_graph=False)
98
+ noisy_grads = x_grad.grad.numpy()
99
+
100
+ gc.collect()
101
+ torch.cuda.empty_cache()
102
+
103
+ assert noisy_grads.shape == x_grad.shape
104
+ return noisy_grads
105
+
106
+
107
+ def smoothgrad_torch(
108
+ input_tensor: torch.Tensor,
109
+ model: Callable[[torch.Tensor], torch.Tensor],
110
+ noise_stdev: float,
111
+ times: int,
112
+ idxs: Optional[TISMLocationsType] = None,
113
+ ) -> torch.Tensor:
114
+ """Custom implementation of SmoothGrad.
115
+ https://arxiv.org/pdf/1706.03825
116
+
117
+ NOTE: For simplicity, for now, we work with SINGLE TENSORS. Assume no batch dimension.
118
+
119
+ This replicates the input `times` times, and runs it through the network all at once.
120
+
121
+ TODO(joelshor): Add batching, for the situation where `times` is larger than the possible batch size
122
+ of a single inference with a network.
123
+ TODO(joelshor): Add ability to efficiently compute multiple inputs at once.
124
+
125
+ Args:
126
+ input_tensor (torch.Tensor): Input tensor. Doesn't have to be genomic. Should NOT be batched.
127
+ model: PyTorch model to use. The model must return a scalar per batch element.
128
+ noise_stdev: Noise to add.
129
+ times: Number of times to add noise.
130
+ idx: If present, only backprop to this location.
131
+
132
+ Returns:
133
+ Per-nucleotide smoothgrad.
134
+ """
135
+ noisy_grads = noisy_grads_torch(
136
+ input_tensor=input_tensor,
137
+ model=model,
138
+ noise_stdev=noise_stdev,
139
+ times=times,
140
+ idxs=idxs,
141
+ )
142
+
143
+ return np.mean(noisy_grads, axis=0)
144
+
145
+
146
+ # TODO(joelshor): Add `attribution_lib.py` test, taken from `malinois/model_def_test.py`.
147
+ def smoothgrad_tensor_to_dict(smooth_grad: torch.Tensor, vocab: list[str]) -> SmoothgradVocabType:
148
+ """Map the smoothgrad indices to the vocab."""
149
+ assert smooth_grad.ndim == 2
150
+ assert list(smooth_grad.shape)[0] == len(vocab)
151
+ def _to_dict(x: torch.Tensor) -> dict[str, torch.Tensor]:
152
+ return {vocab[i]: x[i] for i in range(len(vocab))}
153
+ return [_to_dict(x) for x in smooth_grad.T]
154
+
155
+
156
+ def smoothgrad_to_tism(sg: SmoothgradVocabType, base_seq: str) -> TISMOutputType:
157
+ """Returns result according to Taylor in-silico mutagenesis.
158
+
159
+ Paper: https://www.cell.com/iscience/fulltext/S2589-0042(24)02032-7"""
160
+ assert len(sg) == len(base_seq)
161
+
162
+ tism = []
163
+ for base_nt, sg_dict in zip(base_seq, sg):
164
+ cur_tism = {}
165
+ for nt, sg in sg_dict.items():
166
+ if nt == base_nt: continue
167
+ cur_tism[nt] = float(sg - sg_dict[base_nt])
168
+ tism.append(cur_tism)
169
+
170
+ return tism
171
+
172
+
173
+ def apply_gradient_mask(x: torch.Tensor, idxs: TISMLocationsType) -> tuple[torch.Tensor, torch.Tensor]:
174
+ """Applies a gradient mask to the input tensor.
175
+
176
+ NOTE: Do NOT just multiply by 0. This will run out of memory in large models.
177
+
178
+ Returns:
179
+ Tuple of (x, masked_x), where masked_x is the input tensor with the gradient mask applied.
180
+ """
181
+ assert min(idxs) >= 0
182
+ assert max(idxs) < x.shape[2]
183
+ assert x.ndim == 3, x.shape
184
+
185
+ no_gradient = x.clone().detach()
186
+ no_gradient.requires_grad = False
187
+
188
+ x_grad = x[:, :, idxs].clone().detach()
189
+ x_grad.requires_grad = True
190
+ x_grad_i = {idx: i for i, idx in enumerate(idxs)}
191
+
192
+ # Instead of using `torch.where`, we use this method to make our gradient tensor
193
+ # as small as possible, to preserve memory.
194
+ tensor_slices = [x_grad[:, :, x_grad_i[i]:x_grad_i[i]+1] if i in idxs
195
+ else no_gradient[:, :, i:i+1]
196
+ for i in range(no_gradient.shape[2])]
197
+ x = torch.concat(tensor_slices, dim=2)
198
+
199
+ return x, x_grad
@@ -0,0 +1,175 @@
1
+ """Test attribution_lib.py.
2
+
3
+ To test:
4
+ pytest nucleobench/common/attribution_lib_torch_test.py
5
+ """
6
+
7
+ import pytest
8
+ import numpy as np
9
+ import torch
10
+ from torch import nn
11
+
12
+ from nucleobench.common import attribution_lib_torch
13
+ from nucleobench.common import testing_utils
14
+
15
+ class TestNeuralNetwork(nn.Module):
16
+ def __init__(self):
17
+ super().__init__()
18
+ self.flatten = nn.Flatten()
19
+ self.linear_relu_stack = nn.Sequential(
20
+ nn.Linear(10, 3),
21
+ )
22
+
23
+ def forward(self, x):
24
+ x = self.flatten(x)
25
+ logits = self.linear_relu_stack(x)
26
+ return logits
27
+
28
+
29
+ class Nonlinear(nn.Module):
30
+ def __init__(self):
31
+ super().__init__()
32
+ self.flatten = nn.Flatten()
33
+ self.linear_relu_stack = nn.Sequential(
34
+ nn.Linear(10, 2),
35
+ nn.Sigmoid(),
36
+ )
37
+
38
+ def forward(self, x):
39
+ x = self.flatten(x)
40
+ logits = self.linear_relu_stack(x)
41
+ return logits
42
+
43
+
44
+ class FixedNN(nn.Module):
45
+ def __init__(self):
46
+ super().__init__()
47
+ self.layer_array = np.array([1.0, 2.0])
48
+ self.linear_layer = torch.tensor([self.layer_array], dtype=torch.float).t()
49
+
50
+ def forward(self, x):
51
+ logits = torch.matmul(x, self.linear_layer)
52
+ return logits
53
+
54
+ @pytest.mark.parametrize('times', [1, 3, 5])
55
+ def test_expected_gradient(times):
56
+ """Regardless of number of times, noisy grads should be the same for a linear model."""
57
+ input_tensor = torch.randn(2)
58
+ model = FixedNN()
59
+
60
+ grads = attribution_lib_torch.noisy_grads_torch(
61
+ input_tensor=input_tensor,
62
+ model=model,
63
+ noise_stdev=1.0,
64
+ times=times)
65
+
66
+ assert grads.shape == (times, 2)
67
+ # For linear models, grads should all be the linear layer.
68
+ for i in range(times):
69
+ assert np.array_equal(grads[i, ...], model.layer_array), (
70
+ grads[i, ...], model.layer_array, grads)
71
+
72
+ def test_callable():
73
+ """Check that additional arguments are used."""
74
+ input_tensor = torch.randn(2)
75
+ model = FixedNN()
76
+
77
+ def override_callable(x):
78
+ return 2.0 * model(x)
79
+ grads = attribution_lib_torch.noisy_grads_torch(
80
+ input_tensor=input_tensor,
81
+ model=override_callable,
82
+ noise_stdev=1.0,
83
+ times=3,
84
+ )
85
+
86
+ assert grads.shape == (3, 2)
87
+ # For linear models, grads should all be the linear layer.
88
+ for i in range(3):
89
+ assert np.array_equal(grads[i, ...], [2.0, 4.0]), (
90
+ grads[i, ...], [2.0, 4.0], grads)
91
+
92
+ def test_noisy_grads():
93
+ input_tensor = torch.randn(10)
94
+ model = TestNeuralNetwork()
95
+
96
+ noisy_grads = attribution_lib_torch.noisy_grads_torch(
97
+ input_tensor=input_tensor,
98
+ model=model,
99
+ noise_stdev=0.25,
100
+ times=5)
101
+ assert noisy_grads.shape == (5, 10)
102
+ # For linear models, grads should all be the same.
103
+ for i in range(5):
104
+ assert np.array_equal(noisy_grads[i, ...], noisy_grads[0, ...])
105
+
106
+
107
+ def test_smoothgrad_torch():
108
+ input_tensor = torch.randn(10)
109
+ model = TestNeuralNetwork()
110
+
111
+ grad = attribution_lib_torch.smoothgrad_torch(
112
+ input_tensor=input_tensor,
113
+ model=model,
114
+ noise_stdev=0.25,
115
+ times=5)
116
+
117
+ assert grad.shape == (10,)
118
+
119
+
120
+ def test_noisy_grads_different():
121
+ """In a nonlinear network, gradients should be different."""
122
+ input_tensor = torch.randn(10)
123
+ model = Nonlinear()
124
+
125
+ noisy_grads = attribution_lib_torch.noisy_grads_torch(
126
+ input_tensor=input_tensor,
127
+ model=model,
128
+ noise_stdev=1.0,
129
+ times=5)
130
+ assert noisy_grads.shape == (5, 10)
131
+ # For linear models, grads should all be the same.
132
+ for i in range(1, 5):
133
+ assert not np.array_equal(noisy_grads[i, ...], noisy_grads[0, ...])
134
+
135
+ # TODO(joelshor): Add a unit test for nucleotide-specific TISM.
136
+ def test_smoothgrad_torch_idx_sanity():
137
+ input_tensor = torch.randn(4, 5)
138
+ model = testing_utils.CountLetterModel()
139
+
140
+ grad = attribution_lib_torch.smoothgrad_torch(
141
+ input_tensor=input_tensor,
142
+ model=model.inference_on_tensor,
143
+ noise_stdev=0.0,
144
+ times=1)
145
+ assert grad.shape == (4, 5)
146
+
147
+ for idx in range(5):
148
+ grad_singlebp = attribution_lib_torch.smoothgrad_torch(
149
+ input_tensor=input_tensor,
150
+ model=model.inference_on_tensor,
151
+ noise_stdev=0.25,
152
+ times=5,
153
+ idxs=[idx])
154
+ assert grad_singlebp.shape == (4, 1)
155
+ assert np.all(grad[:, idx] == grad_singlebp[:, 0])
156
+
157
+
158
+ def test_apply_gradient_mask():
159
+ """[idx] and idx should give the same result."""
160
+ input_tensor = torch.randn(1, 10)
161
+ model = TestNeuralNetwork()
162
+ output_tensor = model(input_tensor)
163
+ output_tensor = output_tensor.reshape(1, 1, 3)
164
+
165
+ x1, x_grad = attribution_lib_torch.apply_gradient_mask(
166
+ output_tensor, [0])
167
+ assert x1.shape == (1, 1, 3)
168
+ assert x_grad.shape == (1, 1, 1)
169
+
170
+ x2, x_grad = attribution_lib_torch.apply_gradient_mask(
171
+ output_tensor, [0, 1])
172
+ assert x2.shape == (1, 1, 3)
173
+ assert x_grad.shape == (1, 1, 2)
174
+
175
+ assert (x1 == x2).all(), x1 == x2
@@ -0,0 +1,7 @@
1
+ VOCAB = ['A','C','G','T']
2
+ SERVICE_KEY_FILE_LOCATION = ''
3
+ GCP_OUTPUT_BUCKET_NAME = ''
4
+
5
+
6
+ MPRA_UPSTREAM = 'ACGAAAATGTTGGATGCTCATACTCGTCCTTTTTCAATATTATTGAAGCATTTATCAGGGTTACTAGTACGTCTCTCAAGGATAAGTAAGTAATATTAAGGTACGGGAGGTATTGGACAGGCCGCAATAAAATATCTTTATTTTCATTACATCTGTGTGTTGGTTTTTTGTGTGAATCGATAGTACTAACATACGCTCTCCATCAAAACAAAACGAAACAAAACAAACTAGCAAAATAGGCTGTCCCCAGTGCAAGTGCAGGTGCCAGAACATTTCTCTGGCCTAACTGGCCGCTTGACG'
7
+ MPRA_DOWNSTREAM= 'CACTGCGGCTCCTGCGATCTAACTGGCCGGTACCTGAGCTCGCTAGCCTCGAGGATATCAAGATCTGGCCTCGGCGGCCAAGCTTAGACACTAGAGGGTATATAATGGAAGCTCGACTTCCAGCTTGGCAATCCGGTACTGTTGGTAAAGCCACCATGGTGAGCAAGGGCGAGGAGCTGTTCACCGGGGTGGTGCCCATCCTGGTCGAGCTGGACGGCGACGTAAACGGCCACAAGTTCAGCGTGTCCGGCGAGGGCGAGGGCGATGCCACCTACGGCAAGCTGACCCTGAAGTTCATCT'
@@ -0,0 +1,150 @@
1
+ """Utils for reading from and writing to GCP.
2
+
3
+ To test this with the cloud:
4
+ ```zsh
5
+ python -m nucleobench.common.gcp_utils
6
+ ```
7
+ """
8
+
9
+ from typing import Any, Generator
10
+
11
+ import argparse
12
+ import copy
13
+ import os
14
+ import pickle
15
+ import subprocess
16
+ import torch
17
+ import time
18
+ from google.cloud import storage
19
+
20
+ from nucleobench.common import constants
21
+
22
+
23
+ def get_filepath(
24
+ base_dir: str,
25
+ opt_method: str,
26
+ model: str,
27
+ exp_start_time: str,
28
+ timestamp: str,
29
+ ) -> str:
30
+ return os.path.join(base_dir, f'{opt_method}_{model}', exp_start_time, f'{timestamp}.pkl')
31
+
32
+ def save_proposals(
33
+ write_dicts: list[dict],
34
+ args: argparse.Namespace,
35
+ output_path: str,
36
+ ):
37
+ """
38
+ Save the proposals and associated arguments to a file.
39
+
40
+ This function saves the generated proposals and the input arguments used for their generation
41
+ to a file. The file is named based on the current timestamp and a random tag. The saved file can
42
+ be placed in a local directory or on Google Cloud Storage if a 'gs://' path is provided in the arguments.
43
+
44
+ Args:
45
+ write_dicts: A list of dictiomary of things to write.
46
+ args: Args for the job. Best to keep them close to the output.
47
+ output_path: Directory to write the output to, either locally or on GCP. Format of output is:
48
+ {output_path}/{opt_method}_{model}/{exp start time}/{reults of exp N}.pkl
49
+
50
+ Returns:
51
+ None
52
+ """
53
+ def _tensor2np(obj):
54
+ return obj.detach().clone().numpy() if isinstance(obj, torch.Tensor) else obj
55
+ args = {k: _tensor2np(v) for k, v in vars(args).items()}
56
+ write_dicts = [{k: _tensor2np(v) for k, v in x.items()} for x in write_dicts]
57
+ save_dicts = copy.deepcopy(write_dicts)
58
+
59
+ filename = get_filepath(
60
+ base_dir=output_path,
61
+ opt_method=args['optimization'],
62
+ model=args['model'],
63
+ exp_start_time=save_dicts[0]['exp_starttime_str'],
64
+ timestamp=time.strftime("%Y%m%d_%H%M%S"),
65
+ )
66
+
67
+ # TODO(joelshor): Used to save as torch tensors. Now, saving as a pickled dictionary
68
+ # of numpy arrays. Eventually, save as PyArrow arrays to parquet.
69
+ if filename.startswith('gs://'):
70
+ bucket_name = filename.split('/')[2]
71
+ write_str_to_gcp(
72
+ gcs_output_path=filename,
73
+ content=pickle.dumps(save_dicts),
74
+ binary=True,
75
+ bucket_name=bucket_name,
76
+ )
77
+ else:
78
+ if os.path.dirname(filename) != '' and os.path.dirname(filename) != '.':
79
+ os.makedirs(os.path.dirname(filename), exist_ok=True)
80
+ with open(filename, 'wb') as f:
81
+ pickle.dump(save_dicts, f)
82
+
83
+ print(f'Proposals deposited at:\n\t{filename}')
84
+
85
+
86
+ def get_role_client(service_json_path: str = constants.SERVICE_KEY_FILE_LOCATION):
87
+ try:
88
+ gcp_client = storage.Client.from_service_account_json(service_json_path)
89
+ except FileNotFoundError as e:
90
+ raise ValueError(
91
+ 'GCP service key not found. You are probably trying to write output to a private '
92
+ 'bucket. To write to your own bucket, change the bucket name and service key '
93
+ f'credentials in `nucleobench/common/constants.py`: {service_json_path}') from e
94
+ return gcp_client
95
+
96
+ def write_str_to_gcp(
97
+ gcs_output_path: str,
98
+ content: Any,
99
+ binary: bool,
100
+ bucket_name: str = constants.GCP_OUTPUT_BUCKET_NAME,
101
+ ):
102
+ assert gcs_output_path.startswith('gs://'), 'gcs_output_path must be a GCS path.'
103
+ gcs_output_path = gcs_output_path[len('gs://'):]
104
+ bucket_name, blob_fn = gcs_output_path.split('/', 1)
105
+
106
+ # Instantiates a client.
107
+ storage_client = get_role_client()
108
+ bucket = storage_client.bucket(bucket_name)
109
+ blob = bucket.blob(blob_fn)
110
+
111
+ write_type = 'wb' if binary else 'w'
112
+ with blob.open(write_type) as f:
113
+ f.write(content)
114
+
115
+
116
+ def download_gcp_folder_to_local(dir: str, local_dir: str, bucket: str = constants.GCP_OUTPUT_BUCKET_NAME):
117
+ subprocess.call(['mkdir', '-p', local_dir])
118
+ subprocess.call(['gcloud', 'storage', 'cp', '-r', f'gs://{bucket}/{dir}', local_dir])
119
+
120
+
121
+ def list_files_recursively(local_dir: str) -> Generator[str, None, None]:
122
+ """Recursively lists all files in a given directory."""
123
+ for root, dirs, files in os.walk(local_dir):
124
+ for file in files:
125
+ yield os.path.join(root, file)
126
+
127
+
128
+ if __name__ == '__main__':
129
+ args = {
130
+ 'fake_arg': 'fake_value',
131
+ 'fake_arg2': 'fake_value2',
132
+ 'optimization': 'dummy',
133
+ 'model': 'dummy_model',
134
+ }
135
+ proposal1 = {
136
+ 'string': 'ACTC',
137
+ 'energy': 1.0,
138
+ }
139
+ proposal2 = {
140
+ 'string': 'ACTC',
141
+ 'energy': 2.0,
142
+ }
143
+
144
+ write_dict = {
145
+ 'proposals': [proposal1, proposal2],
146
+ 'exp_starttime_str': '102010',
147
+ }
148
+
149
+ save_proposals([write_dict, write_dict], argparse.Namespace(**args), './gcp_utils_test/dummy')
150
+ save_proposals([write_dict, write_dict], argparse.Namespace(**args), f'gs://{constants.GCP_OUTPUT_BUCKET_NAME}/gcp_utils_test/dummy')
@@ -0,0 +1,10 @@
1
+ """Utilities for managing memory."""
2
+
3
+ import gc
4
+ import torch
5
+
6
+ def free_memory(debug: bool = False):
7
+ collected = gc.collect()
8
+ if debug:
9
+ print(f'[free_memory] Collected: {collected}')
10
+ torch.cuda.empty_cache()