Skip to content

Commit 00933e1

Browse files
authored
Pyomo solving enhancements (#107)
* Ability to run compute-cn when building with HATCHET_BUILD_NOEXT=1 * Some enhancements for pyomo solving * more atomic and faster unit tests
1 parent 702b26c commit 00933e1

19 files changed

Lines changed: 785876 additions & 172 deletions

File tree

.github/workflows/main.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,14 @@ jobs:
9898
PLATFORM: ${{ matrix.python }}
9999
CXXFLAGS: -pthread
100100

101+
# We may end up with files that have a ':' in their filenames,
102+
# due to the <chr>:start:end notation of chromosomes we process through SAMtools/BCFtools.
103+
# upload-artifact is unable to handle these, so we replace colons with dashes
104+
- name: Clean up artifact filenames
105+
run: |
106+
sudo apt install rename
107+
find tests/out -name "*:*" -exec rename 's|:|-|g' {} \;
108+
101109
- name: Save Pytest Output Data
102110
uses: actions/upload-artifact@v2
103111
with:

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def build_extension(self, ext):
7676

7777
setup(
7878
name='hatchet',
79-
version='0.4.8',
79+
version='0.4.9',
8080
packages=['hatchet', 'hatchet.utils', 'hatchet.utils.solve', 'hatchet.bin', 'hatchet.data'],
8181
package_dir={'': 'src'},
8282
package_data={'hatchet': ['hatchet.ini'], 'hatchet.data': ['*']},

src/hatchet/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = '0.4.8'
1+
__version__ = '0.4.9'
22

33
import os.path
44
from importlib.resources import path

src/hatchet/utils/combine_counts.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def main(args=None):
4747
nonzerobaf = (lambda rk : all(sample[4] + sample[5] > 0 for sample in rk))
4848
result = {key : result[key] for key in result if nonzerobaf(result[key])}
4949
for key in sorted(result, key=(lambda x : (sp.numericOrder(x[0]), int(x[1]), int(x[2])))):
50-
for sample in result[key]:
50+
for sample in sorted(result[key]):
5151
sys.stdout.write("{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\n".format(key[0], key[1], key[2], sample[0], sample[1], sample[2], sample[3], sample[4], sample[5], sample[6]))
5252

5353

src/hatchet/utils/solve/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def solve(clonal, seg_file, n, solver='gurobi', solve_mode='cd', d=-1, cn_max=-1
6464

6565
if solve_mode == 'ilp':
6666
ilp = ILPSubset(n, cn_max, d=d, mu=mu, ampdel=ampdel, copy_numbers=copy_numbers, f_a=f_a, f_b=f_b, w=weights)
67-
ilp.create_model()
67+
ilp.create_model(pprint=True)
6868
return ilp.run(solver_type=solver, timelimit=timelimit)
6969
elif solve_mode == 'cd':
7070
cd = CoordinateDescent(f_a=f_a, f_b=f_b, n=n, mu=mu, d=d, cn_max=cn_max, w=weights, ampdel=ampdel,

src/hatchet/utils/solve/cd.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ class CoordinateDescent:
5757
def __init__(self, f_a, f_b, n, mu, d, cn_max, cn, w, ampdel=True):
5858
# ilp attribute used here as a convenient storage container for properties
5959
self.ilp = ILPSubset(n=n, cn_max=cn_max, d=d, mu=mu, ampdel=ampdel, copy_numbers=cn, f_a=f_a, f_b=f_b, w=w)
60+
# Building the model here is not strictly necessary, as, during execution,
61+
# self.carch and c.uarch will copy self.ilp and create+run those models.
62+
# However, we do so here simply so we can print out some diagnostic information once for the user.
63+
self.ilp.create_model(pprint=True)
6064
self.hcA, self.hcB = self.ilp.first_hot_start()
6165

6266
self.seeds = None

src/hatchet/utils/solve/ilp_subset.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import textwrap
12
import math
23
import numpy as np
34
import pandas as pd
@@ -55,6 +56,21 @@ def __copy__(self):
5556
f_a=self.f_a, f_b=self.f_b, w=self.w
5657
)
5758

59+
def __str__(self):
60+
# Pyomo pprint gives us too much information - too unwieldy for large models
61+
# This method is implemented to supply the bare-minimum but useful model information.
62+
if self.model is None:
63+
return ''
64+
else:
65+
return textwrap.dedent(f"""
66+
# ------------------------------------------
67+
# Problem Information
68+
# ------------------------------------------
69+
# Number of constraints: {self.model.nconstraints()}
70+
# Number of variables: {self.model.nvariables()}
71+
# ------------------------------------------
72+
""")
73+
5874
@property
5975
def M(self):
6076
return math.floor(math.log2(self.cn_max)) + 1
@@ -88,7 +104,7 @@ def optimized_u(self):
88104
else:
89105
return self.u
90106

91-
def create_model(self):
107+
def create_model(self, pprint=False):
92108

93109
m, n, k = self.m, self.n, self.k
94110
f_a, f_b = self.f_a, self.f_b
@@ -370,6 +386,9 @@ def create_model(self):
370386
model.obj = pe.Objective(expr=obj, sense=pe.minimize)
371387
self.model = model
372388

389+
if pprint:
390+
print(str(self))
391+
373392
def build_symmetry_breaking(self, model):
374393
for i in range(1, self.n - 1):
375394
_sum1 = 0

src/hatchet/utils/solve/utils.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,17 @@ def parse_clonal(clonal):
3838
cluster_id, cn_a, cn_b = [int(_c) for _c in c.split(':')]
3939

4040
# The first two clonal clusters (used for scaling) MUST have different total copy numbers
41-
if i==1:
41+
if i == 1:
4242
_first_cn_a, _first_cn_b = list(copy_numbers.values())[0]
4343
if _first_cn_a + _first_cn_b == cn_a + cn_b:
4444
raise ValueError('When >= 2 clonal copy numbers are given, the first two must be different in the two segmental clusters')
4545

4646
cn_total = cn_a + cn_b
4747
if (cn_total == 2) and (n_clonal_parts > 1):
48-
warnings.warn('Please specify a single cluster when CN_A+CN_B=2')
48+
# warnings.warn('Please specify a single cluster when CN_A+CN_B=2')
49+
# TODO: The C++ implementation generates a warning corresponding to the above
50+
# This is suppressed by default, which is what we do here for now.
51+
pass
4952
if cluster_id in copy_numbers:
5053
raise ValueError('Already encountered cluster_id =', str(cluster_id))
5154
copy_numbers[cluster_id] = cn_a, cn_b

0 commit comments

Comments
 (0)