Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added linesort two-op operation #266

Merged
merged 14 commits into from
Jun 8, 2021
Prev Previous commit
- improve help text
- removed `--work` (now relies on global `-vv` option)
- added a couple of tests
- CHANGELOG.md updated
  • Loading branch information
abey79 committed Jun 8, 2021
commit ee1c20d9f920b74206034624571f854fa470cf38
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ New features and improvements:
pip install -U vpype # the viewer and its dependencies is NOT installed
```
Forgoing the viewer considerably reduces the number of required dependencies and may be useful for embedded (e.g. Raspberry Pi) and server installs of *vpype*, when the `show` command is not necessary.
* Added optional global optimization feature to `linemerge` (#266, thanks to @tatarize)

This feature is enabled by adding the `--two-opt` option. Since it considerably increases the processing time for complex designs, it should primarily be used for special cases, for example when the same file must be plotted multiple times.

Bug fixes:
* Fixed systematic crash when using the Windows installer (#285)
Expand Down
22 changes: 22 additions & 0 deletions tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,28 @@ def test_linesort_result(runner, opt, expected):
assert data.pen_up_length == pytest.approx(expected)


def test_linesort_reject_bad_opt(runner):
res = runner.invoke(
cli,
"line 0 0 0 10 line 0 10 10 10 line 0 0 10 0 line 10 0 10 10 "
f"linesort --no-flip dbsample dbdump",
)

# in this situation, the greedy optimizer is worse than the starting position, so its
# result should be discarded

data = DebugData.load(res.output)[0]
assert res.exit_code == 0
assert data.pen_up_length == pytest.approx(14.1, abs=0.1)


def test_linesort_two_opt_debug_output(runner, caplog):
res = runner.invoke(cli, "-vv -s 0 random -n 100 linesort --two-opt")

assert res.exit_code == 0
assert "% done with pass" in caplog.text


def test_snap():
line = np.array([0.2, 0.8 + 1.1j, 0.5 + 2.5j])
lc = execute_single_line("snap 1", line)
Expand Down
52 changes: 26 additions & 26 deletions vpype_cli/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,28 +135,29 @@ def linemerge(lines: vp.LineCollection, tolerance: float, no_flip: bool = True):
"--passes",
type=int,
default=250,
help="How many passes is the two-opt algorithm permitted to take?",
)
@click.option(
"-w",
"--work",
is_flag=True,
help="Show work progress within the two-opt algorithm",
help="Number of passes the two-opt algorithm is permitted to take (default: 250)",
)
@vp.layer_processor
def linesort(
lines: vp.LineCollection,
no_flip: bool = True,
two_opt: bool = False,
passes: int = 250,
work: bool = False,
):
def linesort(lines: vp.LineCollection, no_flip: bool, two_opt: bool, passes: int):
"""
Sort lines to minimize the pen-up travel distance.

Note: this process can be lengthy depending on the total number of line. Consider using
`linemerge` before `linesort` to reduce the total number of line and thus significantly
optimizing the overall plotting time.
This command reorders the paths within layers such as to minimize the total pen-up
distance. By default, it will also invert the path direction if it can further optimize the
pen-up distance. This behavior can be disabled using the `--no-flip` option.

By default, a fast, greedy algorithm is used. Although it will dramatically reduce the
pen-up distance in most situation, it trades execution speed for optimality. Further
optimization using the two-opt algorithm can be enabled using the `--two-opt` option. Since
this greatly increase processing time, this feature is mostly useful for special cases such
as when the same design must be plotted multiple times.

When using `--two-opt`, detailed progress indication are available in the debug output,
which is enabled using the `-vv` global option:

$ vpype -vv [...] linesort --two-opt [...]

Note: to further optimize the plotting time, consider using `linemerge` before `linesort`.
"""
if len(lines) < 2:
return lines
Expand Down Expand Up @@ -198,16 +199,18 @@ def linesort(
indexes1 = indexes0 + 1

# noinspection PyShadowingNames
def work_progress(pos):
def log_progress(pos):
# only compute progress if debug output is enable
if logging.getLogger().level > logging.DEBUG:
return
starts = endpoints[indexes0, -1]
ends = endpoints[indexes1, 0]
dists = np.abs(starts - ends)
dist_sum = dists.sum()
logging.info(
logging.debug(
f"optimize: pen-up distance is {dist_sum}. {100 * pos / length:.02f}% done "
f"with pass {current_pass}/{passes}"
)
return dist_sum

improved = True
while improved:
Expand All @@ -224,8 +227,7 @@ def work_progress(pos):
endpoints[: index + 1], (0, 1)
) # top to bottom, and right to left flips.
improved = True
if work:
work_progress(1)
log_progress(1)
for mid in range(1, length - 1):
idxs = np.arange(mid, length - 1)

Expand All @@ -245,8 +247,7 @@ def work_progress(pos):
endpoints[mid : mid + index + 1], (0, 1)
)
improved = True
if work:
work_progress(mid)
log_progress(mid)

last = endpoints[-1, -1]
pen_ups = endpoints[indexes0, -1]
Expand All @@ -259,8 +260,7 @@ def work_progress(pos):
endpoints[index + 1 :], (0, 1)
) # top to bottom, and right to left flips.
improved = True
if work:
work_progress(length)
log_progress(length)
if current_pass >= passes:
break
current_pass += 1
Expand Down