Skip to content

Commit 28558bf

Browse files
committed
Merge branch 'develop'
2 parents ce949fb + 3d3952d commit 28558bf

File tree

9 files changed

+607
-62
lines changed

9 files changed

+607
-62
lines changed

README.md

+9-3
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@
1010
[![Project Status: Active – The project has reached a stable, usable state and is being actively developed.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active)
1111
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/automata-lib)
1212
[![status](https://joss.theoj.org/papers/fe4d8521383598038e38bc0c948718af/status.svg)](https://joss.theoj.org/papers/fe4d8521383598038e38bc0c948718af)
13+
[![pyOpenSci](https://tinyurl.com/y22nb8up)](https://github.com/pyOpenSci/software-submission/issues/152)
1314

14-
- **Documentation**: https://caleb531.github.io/automata/
15-
- **Migration Guide**: https://caleb531.github.io/automata/migration/
16-
- **API**: https://caleb531.github.io/automata/api/
15+
Links:
16+
- [**Documentation**](https://caleb531.github.io/automata/)
17+
- [**Examples**](https://caleb531.github.io/automata/examples/fa-examples/)
18+
- [**Migration Guide**](https://caleb531.github.io/automata/migration/)
19+
- [**API**](https://caleb531.github.io/automata/api/)
1720

1821
Automata is a Python 3 library implementing structures and algorithms for manipulating finite automata,
1922
pushdown automata, and Turing machines. The algorithms have been optimized and are capable of
@@ -57,6 +60,9 @@ To install the optional visual dependencies, use the `visual` extra:
5760
pip install 'automata-lib[visual]'
5861
```
5962

63+
If you encounter errors building `pygraphviz`, you may need to install `graphviz`.
64+
See the instructions [here](https://graphviz.org/download/).
65+
6066
## Contributing
6167

6268
Contributions are always welcome! Take a look at the [contributing guide](./CONTRIBUTING.md).

automata/fa/dfa.py

+76-9
Original file line numberDiff line numberDiff line change
@@ -278,9 +278,8 @@ def to_partial(self, *, retain_names: bool = False, minify: bool = True) -> Self
278278
Self
279279
An equivalent partial DFA.
280280
"""
281-
if self.allow_partial:
282-
return self.copy()
283281

282+
# Still want to try and remove dead states in the case of a partial DFA
284283
graph = self._get_digraph()
285284
live_states = get_reachable_nodes(graph, [self.initial_state])
286285
non_trap_states = get_reachable_nodes(graph, self.final_states, reversed=True)
@@ -338,7 +337,12 @@ def to_complete(self, trap_state: Optional[DFAStateT] = None) -> Self:
338337
An equivalent complete DFA.
339338
340339
"""
341-
if not self.allow_partial:
340+
is_partial = any(
341+
len(lookup) != len(self.input_symbols)
342+
for lookup in self.transitions.values()
343+
)
344+
345+
if not is_partial:
342346
return self.copy()
343347

344348
if trap_state is None:
@@ -624,7 +628,9 @@ def _minify(
624628
x for x in count(-1, -1) if x not in reachable_states
625629
)
626630
for trap_symbol in input_symbols:
627-
transition_back_map[trap_symbol][trap_state] = []
631+
transition_back_map[trap_symbol][trap_state] = [
632+
trap_state
633+
]
628634

629635
reachable_states.add(trap_state)
630636

@@ -681,6 +687,11 @@ def _minify(
681687
if trap_state not in eq
682688
}
683689

690+
# If only one equivalence class with the trap state,
691+
# return empty language.
692+
if not back_map:
693+
return cls.empty_language(input_symbols)
694+
684695
new_input_symbols = input_symbols
685696
new_states = frozenset(back_map.values())
686697
new_initial_state = back_map[initial_state]
@@ -1276,6 +1287,8 @@ def predecessor(
12761287
*,
12771288
strict: bool = True,
12781289
key: Optional[Callable[[Any], Any]] = None,
1290+
min_length: int = 0,
1291+
max_length: Optional[int] = None,
12791292
) -> Optional[str]:
12801293
"""
12811294
Returns the first string accepted by the DFA that comes before
@@ -1291,6 +1304,10 @@ def predecessor(
12911304
key : Optional[Callable], default: None
12921305
Function for defining custom lexicographical ordering. Defaults to using
12931306
the standard string ordering.
1307+
min_length : int, default: 0
1308+
Limits generation to words with at least the given length.
1309+
max_length : Optional[int], default: None
1310+
Limits generation to words with at most the given length.
12941311
12951312
Returns
12961313
------
@@ -1303,7 +1320,13 @@ def predecessor(
13031320
Raised if the language accepted by self is infinite, as we cannot
13041321
generate predecessors in this case.
13051322
"""
1306-
for word in self.predecessors(input_str, strict=strict, key=key):
1323+
for word in self.predecessors(
1324+
input_str,
1325+
strict=strict,
1326+
key=key,
1327+
min_length=min_length,
1328+
max_length=max_length,
1329+
):
13071330
return word
13081331
return None
13091332

@@ -1313,6 +1336,8 @@ def predecessors(
13131336
*,
13141337
strict: bool = True,
13151338
key: Optional[Callable[[Any], Any]] = None,
1339+
min_length: int = 0,
1340+
max_length: Optional[int] = None,
13161341
) -> Generator[str, None, None]:
13171342
"""
13181343
Generates all strings that come before the input string
@@ -1328,6 +1353,10 @@ def predecessors(
13281353
key : Optional[Callable], default: None
13291354
Function for defining custom lexicographical ordering. Defaults to using
13301355
the standard string ordering.
1356+
min_length : int, default: 0
1357+
Limits generation to words with at least the given length.
1358+
max_length : Optional[int], default: None
1359+
Limits generation to words with at most the given length.
13311360
13321361
Returns
13331362
------
@@ -1341,14 +1370,23 @@ def predecessors(
13411370
Raised if the language accepted by self is infinite, as we cannot
13421371
generate predecessors in this case.
13431372
"""
1344-
yield from self.successors(input_str, strict=strict, reverse=True, key=key)
1373+
yield from self.successors(
1374+
input_str,
1375+
strict=strict,
1376+
reverse=True,
1377+
key=key,
1378+
min_length=min_length,
1379+
max_length=max_length,
1380+
)
13451381

13461382
def successor(
13471383
self,
13481384
input_str: Optional[str],
13491385
*,
13501386
strict: bool = True,
13511387
key: Optional[Callable[[Any], Any]] = None,
1388+
min_length: int = 0,
1389+
max_length: Optional[int] = None,
13521390
) -> Optional[str]:
13531391
"""
13541392
Returns the first string accepted by the DFA that comes after
@@ -1364,13 +1402,23 @@ def successor(
13641402
key : Optional[Callable], default: None
13651403
Function for defining custom lexicographical ordering. Defaults to using
13661404
the standard string ordering.
1405+
min_length : int, default: 0
1406+
Limits generation to words with at least the given length.
1407+
max_length : Optional[int], default: None
1408+
Limits generation to words with at most the given length.
13671409
13681410
Returns
13691411
------
13701412
str
13711413
The first string accepted by the DFA lexicographically before input_string.
13721414
"""
1373-
for word in self.successors(input_str, strict=strict, key=key):
1415+
for word in self.successors(
1416+
input_str,
1417+
strict=strict,
1418+
key=key,
1419+
min_length=min_length,
1420+
max_length=max_length,
1421+
):
13741422
return word
13751423
return None
13761424

@@ -1381,6 +1429,8 @@ def successors(
13811429
strict: bool = True,
13821430
key: Optional[Callable[[Any], Any]] = None,
13831431
reverse: bool = False,
1432+
min_length: int = 0,
1433+
max_length: Optional[int] = None,
13841434
) -> Generator[str, None, None]:
13851435
"""
13861436
Generates all strings that come after the input string
@@ -1398,6 +1448,10 @@ def successors(
13981448
the standard string ordering.
13991449
reverse : bool, default: False
14001450
If True, then predecessors will be generated instead of successors.
1451+
min_length : int, default: 0
1452+
Limits generation to words with at least the given length.
1453+
max_length : Optional[int], default: None
1454+
Limits generation to words with at most the given length.
14011455
14021456
Returns
14031457
------
@@ -1446,6 +1500,8 @@ def successors(
14461500
if (
14471501
not reverse
14481502
and should_yield
1503+
and min_length <= len(char_stack)
1504+
and (max_length is None or len(char_stack) <= max_length)
14491505
and candidate == first_symbol
14501506
and state in self.final_states
14511507
):
@@ -1456,7 +1512,9 @@ def successors(
14561512
else self._get_next_current_state(state, candidate)
14571513
)
14581514
# Traverse to child if candidate is viable
1459-
if candidate_state in coaccessible_nodes:
1515+
if candidate_state in coaccessible_nodes and (
1516+
max_length is None or len(char_stack) < max_length
1517+
):
14601518
state_stack.append(candidate_state)
14611519
char_stack.append(cast(str, candidate))
14621520
candidate = first_symbol
@@ -1465,6 +1523,8 @@ def successors(
14651523
if (
14661524
reverse
14671525
and should_yield
1526+
and min_length <= len(char_stack)
1527+
and (max_length is None or len(char_stack) <= max_length)
14681528
and candidate is None
14691529
and state in self.final_states
14701530
):
@@ -1480,6 +1540,8 @@ def successors(
14801540
if (
14811541
reverse
14821542
and should_yield
1543+
and min_length <= len(char_stack)
1544+
and (max_length is None or len(char_stack) <= max_length)
14831545
and candidate is None
14841546
and state in self.final_states
14851547
):
@@ -1707,13 +1769,18 @@ def from_prefix(
17071769

17081770
states = frozenset(transitions.keys())
17091771
final_states = {last_state}
1772+
1773+
is_partial = any(
1774+
len(lookup) != len(input_symbols) for lookup in transitions.values()
1775+
)
1776+
17101777
return cls(
17111778
states=states,
17121779
input_symbols=input_symbols,
17131780
transitions=transitions,
17141781
initial_state=0,
17151782
final_states=final_states if contains else states - final_states,
1716-
allow_partial=as_partial,
1783+
allow_partial=is_partial,
17171784
)
17181785

17191786
@classmethod

0 commit comments

Comments
 (0)