• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Edinburgh-Genome-Foundry / DnaChisel / 14225166311

02 Apr 2025 04:53PM UTC coverage: 90.508% (+0.5%) from 90.054%
14225166311

push

github

veghp
Switch to Sphinx

2994 of 3308 relevant lines covered (90.51%)

0.91 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.13
/dnachisel/DnaOptimizationProblem/mixins/ConstraintsSolverMixin.py
1
from ...Location import Location
1✔
2
from ...Specification.SpecEvaluation import (
1✔
3
    ProblemConstraintsEvaluations,
4
)
5
from ..NoSolutionError import NoSolutionError
1✔
6

7

8
class ConstraintsSolverMixin:
1✔
9
    @property
1✔
10
    def constraints_before(self):
1✔
11
        """"""
12
        if self._constraints_before is None:
1✔
13
            sequence = self.sequence
1✔
14
            self.sequence = self.sequence_before
1✔
15
            self._constraints_before = self.constraints_evaluations()
1✔
16
            self.sequence = sequence
1✔
17
        return self._constraints_before
1✔
18

19
    def constraints_evaluations(self, autopass=True):
1✔
20
        """Return a list of the evaluations of each constraint of the problem.
21

22
        The "autopass" enables to just assume that constraints
23
        enforced by the mutation space are verified.
24
        """
25
        return ProblemConstraintsEvaluations.from_problem(
1✔
26
            self, autopass_constraints=autopass
27
        )
28

29
    def all_constraints_pass(self, autopass=True):
1✔
30
        """Return whether the current problem sequence passes all constraints."""
31
        if len(self.constraints) == 0:
1✔
32
            return True
1✔
33
        return all(
1✔
34
            c.evaluate(self).passes
35
            for c in self.constraints
36
            if (not autopass) or (not c.enforced_by_nucleotide_restrictions)
37
        )
38

39
    def constraints_text_summary(self, failed_only=False, autopass=True):
1✔
40
        evals = self.constraints_evaluations(autopass=autopass)
1✔
41
        if failed_only:
1✔
42
            evals = evals.filter("failing")
×
43
        return evals.to_text()
1✔
44

45
    def get_focus_constraint(self):
1✔
46
        focus_constraints = [c for c in self.constraints if c.is_focus]
1✔
47
        if len(focus_constraints) == 1:
1✔
48
            focus = focus_constraints[0]
1✔
49
            other_constraints = [c for c in self.constraints if not c.is_focus]
1✔
50
            return focus, other_constraints
1✔
51
        return None, None
1✔
52

53
    def resolve_constraints_by_exhaustive_search(self):
1✔
54
        """Solve all constraints by exploring the whole search space.
55

56
        This method iterates over ``self.iter_mutations_space()`` (space of
57
        all sequences that could be reached through successive mutations) and
58
        stops when it finds a sequence which meets all the constraints of the
59
        problem.
60
        """
61
        focus_constraint, other_constraints = self.get_focus_constraint()
1✔
62
        sequence_before = self.sequence
1✔
63
        all_variants = self.mutation_space.all_variants(self.sequence)
1✔
64
        space_size = int(self.mutation_space.space_size)
1✔
65
        self.logger(mutation__total=space_size)
1✔
66
        for variant in self.logger.iter_bar(mutation=all_variants):
1✔
67
            self.sequence = variant
1✔
68
            if focus_constraint is not None:
1✔
69
                if focus_constraint.evaluate(self).passes:
1✔
70
                    if all(c.evaluate(self).passes for c in other_constraints):
1✔
71
                        self.logger(mutation__index=space_size)
1✔
72
                        return
1✔
73
            elif self.all_constraints_pass():
×
74
                self.logger(mutation__index=space_size)
×
75
                return
×
76
        self.sequence = sequence_before
1✔
77
        raise NoSolutionError(
1✔
78
            "Exhaustive search failed to satisfy all constraints.",
79
            problem=self,
80
        )
81

82
    def resolve_constraints_by_random_mutations(self):
1✔
83
        """Solve all constraints by successive sets of random mutations.
84

85
        This method modifies the problem sequence by applying a number
86
        ``mutations_per_iteration`` of random mutations. The constraints are
87
        then evaluated on the new sequence. If all constraints pass, the new
88
        sequence becomes the problem's new sequence.
89
        If not all constraints pass, the sum of all scores from failing
90
        constraints is considered. If this score is superior to the score of
91
        the previous sequence, the new sequence becomes the problem's new
92
        sequence.
93

94
        This operation is repeated `max_iter` times at most, after which
95
        a ``NoSolutionError`` is thrown if no solution was found.
96
        """
97

98
        focus_constraint, other_constraints = self.get_focus_constraint()
1✔
99
        if focus_constraint is not None:
1✔
100
            self.resolve_single_constraint_by_random_mutations(
1✔
101
                focus_constraint, other_constraints
102
            )
103
            return
1✔
104

105
        evaluations = self.constraints_evaluations()
1✔
106
        score = sum([e.score for e in evaluations if not e.passes])
1✔
107
        iters = range(self.max_random_iters)
1✔
108
        for i in self.logger.iter_bar(mutation=iters):
1✔
109

110
            if all(e.passes for e in evaluations):
1✔
111
                self.logger(mutation__index=iters)
1✔
112
                return
1✔
113
            previous_sequence = self.sequence
1✔
114
            self.sequence = self.mutation_space.apply_random_mutations(
1✔
115
                n_mutations=self.mutations_per_iteration,
116
                sequence=self.sequence,
117
            )
118

119
            evaluations = self.constraints_evaluations()
1✔
120
            new_score = sum([e.score for e in evaluations if not e.passes])
1✔
121

122
            if new_score > score:
1✔
123
                score = new_score
1✔
124
            else:
125
                self.sequence = previous_sequence
1✔
126
        raise NoSolutionError(
×
127
            "Random search did not find a solution in the given number of "
128
            "attempts. Try to increase the number of attempts with:\n\n"
129
            "problem.max_random_iters = 5000 # or even 10000, 20000, etc.\n\n"
130
            "If the problem persists, you may be in presence of a complex or "
131
            "unsolvable problem.",
132
            problem=self,
133
        )
134

135
    def resolve_single_constraint_by_random_mutations(
1✔
136
        self, constraint, other_constraints
137
    ):
138
        evaluation = constraint.evaluation
1✔
139
        score = evaluation.score
1✔
140
        iters = range(self.max_random_iters)
1✔
141
        for i in self.logger.iter_bar(mutation=iters):
1✔
142
            previous_sequence = self.sequence
1✔
143
            self.sequence = self.mutation_space.apply_random_mutations(
1✔
144
                n_mutations=self.mutations_per_iteration,
145
                sequence=self.sequence,
146
            )
147
            new_evaluation = constraint.evaluate(self)
1✔
148
            if new_evaluation.score > score:
1✔
149
                if all(c.evaluate(self).passes for c in other_constraints):
1✔
150
                    score = new_evaluation.score
1✔
151
                    if new_evaluation.passes:
1✔
152
                        self.logger(mutation__index=iters)
1✔
153
                        return
1✔
154
                else:
155
                    self.sequence = previous_sequence
1✔
156
            else:
157
                self.sequence = previous_sequence
1✔
158

159
        raise NoSolutionError(
1✔
160
            "Random search did not find a solution in the given number of "
161
            "attempts. Try to increase the number of attempts with:\n\n"
162
            "problem.max_random_iters = 5000 # or even 10000, 20000, etc.\n\n"
163
            "If the problem persists, you may be in presence of a complex or "
164
            "unsolvable problem.",
165
            problem=self,
166
        )
167

168
    def resolve_constraints_locally(self):
1✔
169
        """Perform a local search, either stochastic or exhaustive."""
170
        if self.mutation_space.space_size < self.randomization_threshold:
1✔
171
            self.resolve_constraints_by_exhaustive_search()
1✔
172
        else:
173
            self.resolve_constraints_by_random_mutations()
1✔
174

175
    def resolve_constraint(self, constraint):
1✔
176
        """Resolve a constraint through successive localizations."""
177

178
        # EVALUATE THE CONSTRAINT, FIND BREACHING LOCATIONS
179

180
        evaluation = constraint.evaluate(self)
1✔
181
        if evaluation.passes:
1✔
182
            return
1✔
183

184
        locations = sorted(evaluation.locations)
1✔
185
        iterator = self.logger.iter_bar(
1✔
186
            location=locations, bar_message=lambda loc: str(loc)
187
        )
188

189
        # FOR EACH LOCATION, CREATE A LOCAL PROBLEM AND RESOLVE LOCALLY.
190

191
        for i, location in enumerate(iterator):
1✔
192

193
            # SEVERAL "EXTENSIONS" OF THE LOCAL ZONE WILL BE TESTED IN TURN
194
            # IN CASE THE LOCAL SEQUENCE IS FROZEN DUE TO NUCLEOTIDE INTER-
195
            # DEPENDENCIES (CODONS, ETC.)
196

197
            for extension in self.local_extensions:
1✔
198
                new_location = location.extended(extension)
1✔
199
                mutation_space = self.mutation_space.localized(new_location)
1✔
200

201
                if mutation_space.space_size == 0:
1✔
202

203
                    # If the sequence is frozen at this location, either
204
                    # "continue" (go straight to the next, larger extension)
205
                    # or if we are already in the largest extension, return
206
                    # an error with data that will be used by the report
207
                    # generator.
208

209
                    if extension != self.local_extensions[-1]:
1✔
210
                        continue
1✔
211
                    else:
212
                        error = NoSolutionError(
1✔
213
                            location=new_location,
214
                            problem=self,
215
                            message="Constraint breach in region that cannot "
216
                            "be mutated.",
217
                        )
218
                        error.location = new_location
1✔
219
                        error.constraint = constraint
1✔
220
                        error.message = "While solving %s in %s:\n\n%s" % (
1✔
221
                            constraint,
222
                            new_location,
223
                            str(error),
224
                        )
225
                        self.logger(
1✔
226
                            location__index=len(locations),
227
                            location__message="Cold exit",
228
                        )
229
                        raise error
1✔
230
                new_location = Location(*mutation_space.choices_span)
1✔
231

232
                # This blocks solves the problem of overlapping breaches,
233
                # which can make the local optimization impossible.
234
                # If the next constraint breach overlaps with the current
235
                # location, localize the constraint with a with_righthand=False
236
                # flag, which will be used by the constraints ".localized"
237
                # method to only consider the right-hand side.
238

239
                if (i < (len(locations) - 1)) and (
1✔
240
                    locations[i + 1].overlap_region(new_location)
241
                ):
242
                    this_local_constraint = constraint.localized(
1✔
243
                        new_location, with_righthand=False, problem=self
244
                    )
245
                else:
246
                    this_local_constraint = constraint.localized(
1✔
247
                        new_location, problem=self
248
                    )
249
                evaluation = this_local_constraint.evaluate(self)
1✔
250

251
                # MAYBE THE LOCAL BREACH WAS ALREADY RESOLVED AS A SIDE EFFECT
252
                # OF SOLVING PREVIOUS BREACHES. IN THAT CASE, PASS.
253

254
                if evaluation.passes:
1✔
255
                    continue
1✔
256

257
                # ELSE, CREATE A NEW LOCAL PROBLEM WITH LOCALIZED CONSTRAINTS
258

259
                this_local_constraint.is_focus = True
1✔
260
                this_local_constraint.evaluation = evaluation
1✔
261

262
                localized_constraints = [
1✔
263
                    cst.localized(new_location, problem=self)
264
                    for cst in self.constraints
265
                    if cst != constraint and not cst.enforced_by_nucleotide_restrictions
266
                ]
267
                passing_localized_constraints = [
1✔
268
                    cst
269
                    for cst in localized_constraints
270
                    if cst is not None and cst.evaluate(self).passes
271
                ]
272
                local_problem = self.__class__(
1✔
273
                    sequence=self.sequence,
274
                    constraints=(
275
                        [this_local_constraint] + passing_localized_constraints
276
                    ),
277
                    mutation_space=mutation_space,
278
                )
279
                local_problem.randomization_threshold = self.randomization_threshold
1✔
280
                local_problem.max_random_iters = self.max_random_iters
1✔
281
                local_problem.mutations_per_iteration = self.mutations_per_iteration
1✔
282

283
                # STORE THE LOCAL PROBLEM IN THE LOGGER.
284
                # This is useful for troubleshooting.
285

286
                self.logger.store(
1✔
287
                    problem=self,
288
                    local_problem=local_problem,
289
                    location=location,
290
                )
291

292
                # RESOLVE THE LOCAL PROBLEM. RETURN AN ERROR IF IT FAILS.
293

294
                try:
1✔
295
                    if hasattr(constraint, "resolution_heuristic"):
1✔
296
                        constraint.resolution_heuristic(local_problem)
1✔
297
                    else:
298
                        local_problem.resolve_constraints_locally()
1✔
299
                    self._replace_sequence(local_problem.sequence)
1✔
300
                    break
1✔
301
                except NoSolutionError as error:
1✔
302
                    if extension == self.local_extensions[-1]:
1✔
303
                        error.location = new_location
1✔
304
                        error.constraint = constraint
1✔
305
                        error.message = "While solving %s in %s:\n\n%s" % (
1✔
306
                            constraint,
307
                            new_location,
308
                            str(error),
309
                        )
310
                        self.logger(
1✔
311
                            location__index=len(locations),
312
                            location__message="Cold exit",
313
                        )
314
                        raise error
1✔
315
                    else:
316
                        continue
1✔
317

318
    def resolve_constraints(self, final_check=True, cst_filter=None):
1✔
319
        """Solve a particular constraint using local, targeted searches.
320

321
        Parameters
322
        ----------
323

324
        constraint
325
          The ``Specification`` object for which the sequence should be solved
326

327
        final_check
328
          If True, a final check of that all constraints pass will be run at
329
          the end of the process, when constraints have been resolved one by
330
          one, to check that the solving of one constraint didn't undo the
331
          solving of another.
332

333
        cst_filter
334
          An optional filter to only resolve a subset function (constraint => True/False)
335

336
        """
337
        constraints = [
1✔
338
            c
339
            for c in self.constraints
340
            if not c.enforced_by_nucleotide_restrictions
341
            and ((cst_filter is None) or cst_filter(c))
342
        ]
343
        if len(constraints) == 0:
1✔
344
            return
1✔
345
        constraints = sorted(constraints, key=lambda c: -c.priority)
1✔
346
        for constraint in self.logger.iter_bar(
1✔
347
            constraint=constraints, bar_message=lambda c: str(c)
348
        ):
349
            try:
1✔
350
                self.resolve_constraint(constraint=constraint)
1✔
351
            except NoSolutionError as error:
1✔
352
                self.logger(constraint__index=len(constraints))
1✔
353
                raise error
1✔
354
        if final_check:
1✔
355
            self.perform_final_constraints_check()
1✔
356

357
    def perform_final_constraints_check(self):
1✔
358
        for cst in self.constraints:
1✔
359
            if not cst.evaluate(self).passes:
1✔
360
                raise NoSolutionError(
×
361
                    "The solving of all constraints failed to solve"
362
                    " all constraints, as some appear unsolved at the end"
363
                    " of the optimization. This is an unintended behavior,"
364
                    " likely due to a complex problem. Try running the"
365
                    " solver on the same sequence again, or report the"
366
                    " error to the maintainers:\n\n"
367
                    + self.constraints_text_summary(failed_only=True, autopass=False),
368
                    problem=self,
369
                )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc