base-loom-server 1.2b2__tar.gz → 1.2b4__tar.gz

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.
Files changed (103) hide show
  1. {base_loom_server-1.2b2/src/base_loom_server.egg-info → base_loom_server-1.2b4}/PKG-INFO +1 -1
  2. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/docs/version_history.md +8 -0
  3. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/docs/weaving_tabby.md +1 -1
  4. base_loom_server-1.2b4/src/base_loom_server/compute_tabby.py +249 -0
  5. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/reduced_pattern.py +23 -11
  6. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/testutils.py +4 -1
  7. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/utils.py +10 -1
  8. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/version.py +1 -1
  9. {base_loom_server-1.2b2 → base_loom_server-1.2b4/src/base_loom_server.egg-info}/PKG-INFO +1 -1
  10. base_loom_server-1.2b4/tests/test_compute_tabby.py +109 -0
  11. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/tests/test_utils.py +34 -14
  12. base_loom_server-1.2b2/src/base_loom_server/compute_tabby.py +0 -183
  13. base_loom_server-1.2b2/tests/test_compute_tabby.py +0 -98
  14. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/.github/workflows/deploy_to_pypi.yml +0 -0
  15. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/.github/workflows/run_pytest.yml +0 -0
  16. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/.github/workflows/serve_docs.yml +0 -0
  17. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/.gitignore +0 -0
  18. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/.pre-commit-config.yaml +0 -0
  19. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/LICENSE.txt +0 -0
  20. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/Manifest.in +0 -0
  21. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/README.md +0 -0
  22. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/docs/32-shaft twill.wif +0 -0
  23. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/docs/coding.md +0 -0
  24. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/docs/images/screen_shots/settings_safari_iphone_mini.jpg +0 -0
  25. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/docs/images/screen_shots/settings_safari_macos.jpg +0 -0
  26. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/docs/images/screen_shots/show_right_panel_icon.jpg +0 -0
  27. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/docs/images/screen_shots/threading_safari_iphone_mini.jpg +0 -0
  28. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/docs/images/screen_shots/threading_safari_macos.jpg +0 -0
  29. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/docs/images/screen_shots/translate_icon.jpg +0 -0
  30. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/docs/images/screen_shots/translate_panel.jpg +0 -0
  31. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/docs/images/screen_shots/weaving_safari_iphone_mini.jpg +0 -0
  32. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/docs/images/screen_shots/weaving_safari_macos.jpg +0 -0
  33. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/docs/images/screen_shots/weaving_tabby_safari_iphone_mini.jpg +0 -0
  34. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/docs/images/screen_shots/weaving_tabby_safari_macos.jpg +0 -0
  35. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/docs/index.md +0 -0
  36. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/docs/installing.md +0 -0
  37. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/docs/settings.md +0 -0
  38. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/docs/threading.md +0 -0
  39. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/docs/translations.md +0 -0
  40. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/docs/weaving.md +0 -0
  41. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/mkdocs.yml +0 -0
  42. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/pyproject.toml +0 -0
  43. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/setup.cfg +0 -0
  44. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/__init__.py +0 -0
  45. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/app_runner.py +0 -0
  46. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/base_loom_server.py +0 -0
  47. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/base_mock_loom.py +0 -0
  48. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/check_translation_files.py +0 -0
  49. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/client_replies.py +0 -0
  50. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/constants.py +0 -0
  51. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/display.css +0 -0
  52. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/display.html +0 -0
  53. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/display.js +0 -0
  54. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/enums.py +0 -0
  55. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/example_loom_server.py +0 -0
  56. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/example_mock_loom.py +0 -0
  57. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/favicon-32x32.png +0 -0
  58. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/locales/Dansk.json +0 -0
  59. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/locales/Deutsch.json +0 -0
  60. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/locales/English.json +0 -0
  61. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/locales/Espa/303/261ol.json" +0 -0
  62. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/locales/Fran/303/247ais.json" +0 -0
  63. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/locales/Italiano.json +0 -0
  64. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/locales/Nederlands.json +0 -0
  65. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/locales/Norsk.json +0 -0
  66. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/locales/Suomi.json +0 -0
  67. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/locales/Svenska.json +0 -0
  68. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/locales/default.json +0 -0
  69. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/main.py +0 -0
  70. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/mock_streams.py +0 -0
  71. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/nmcli_wifi.py +0 -0
  72. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/pattern_database.py +0 -0
  73. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/py.typed +0 -0
  74. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/rename_crowdin_files.py +0 -0
  75. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/test_data/pattern_files/eighteen shaft liftplan.wif +0 -0
  76. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/test_data/pattern_files/many color liftplan and zeros.dtx +0 -0
  77. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/test_data/pattern_files/many color liftplan and zeros.wif +0 -0
  78. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/test_data/pattern_files/many color multiple treadles and zeros.dtx +0 -0
  79. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/test_data/pattern_files/many color multiple treadles and zeros.wif +0 -0
  80. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/test_data/pattern_files/many color multiple treadles and zeros.wpo +0 -0
  81. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/test_data/pattern_files/many color single treadles.dtx +0 -0
  82. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/test_data/pattern_files/many color single treadles.wif +0 -0
  83. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/test_data/pattern_files/many color single treadles.wpo +0 -0
  84. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/test_data/pattern_files/two color liftplan.dtx +0 -0
  85. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/test_data/pattern_files/two color liftplan.wif +0 -0
  86. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/test_data/pattern_files/two color multiple treadles.dtx +0 -0
  87. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/test_data/pattern_files/two color multiple treadles.wif +0 -0
  88. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/test_data/pattern_files/two color single treadles.dtx +0 -0
  89. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/test_data/pattern_files/two color single treadles.wif +0 -0
  90. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/test_data/translation_files/extra_keys.json +0 -0
  91. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server/translations.py +0 -0
  92. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server.egg-info/SOURCES.txt +0 -0
  93. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server.egg-info/dependency_links.txt +0 -0
  94. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server.egg-info/entry_points.txt +0 -0
  95. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server.egg-info/requires.txt +0 -0
  96. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/src/base_loom_server.egg-info/top_level.txt +0 -0
  97. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/tests/test_loom_server.py +0 -0
  98. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/tests/test_mock_loom.py +0 -0
  99. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/tests/test_mock_streams.py +0 -0
  100. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/tests/test_pattern_database.py +0 -0
  101. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/tests/test_reduced_pattern.py +0 -0
  102. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/tests/test_translations.py +0 -0
  103. {base_loom_server-1.2b2 → base_loom_server-1.2b4}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: base_loom_server
3
- Version: 1.2b2
3
+ Version: 1.2b4
4
4
  Summary: Base package for web servers that control dobby multi-shaft looms
5
5
  Author-email: Russell Owen <r3owen@gmail.com>
6
6
  Project-URL: Homepage, https://pypi.org/project/base-loom-server/
@@ -1,5 +1,13 @@
1
1
  # Version History
2
2
 
3
+ ## 1.2b4 2026-03-07
4
+
5
+ * Further improve computation of tabby.
6
+
7
+ ## 1.2b3 2026-03-06
8
+
9
+ * Improve computation of tabby.
10
+
3
11
  ## 1.2b2 2026-03-05
4
12
 
5
13
  * Fix initial display of tabby panel.
@@ -7,7 +7,7 @@
7
7
  </div>
8
8
 
9
9
  The **Tabby** mode allows you to weave tabby (plain weave), or a close approximation.
10
- This can be used to weaving hems or item separators.
10
+ This can be used to weave hems or item separators.
11
11
 
12
12
  Caveats:
13
13
 
@@ -0,0 +1,249 @@
1
+ import functools
2
+ import itertools
3
+ import operator
4
+
5
+ from .utils import prune_duplicates
6
+
7
+ # Maximum number of starting points for iterations in compute_tabby_shaft_word1.
8
+ # Avoid very large values to keep compute time reasonable.
9
+ MAX_ITER_TABBY1 = 5
10
+
11
+
12
+ def compute_num_transitions(tabby_shaft_word: int, threading: list[int]) -> int:
13
+ """Compute the number of transitions produced by a tabby shaft word.
14
+
15
+ The largest possible value is len(threading_shaft_words) - 1.
16
+
17
+ Args:
18
+ tabby_shaft_word: Tabby shaft word.
19
+ threading: Shaft index (0-based) for each warp end.
20
+ Negative values are ignored (and adjacent duplicate values
21
+ naturally do not affect the result).
22
+ """
23
+ threading_shaft_words = [1 << shaft for shaft in threading if shaft >= 0]
24
+ return _compute_num_transitions_impl(
25
+ tabby_shaft_word=tabby_shaft_word, threading_shaft_words=threading_shaft_words
26
+ )
27
+
28
+
29
+ def _compute_num_transitions_impl(tabby_shaft_word: int, threading_shaft_words: list[int]) -> int:
30
+ """Implementation of compute_num_transitions.
31
+
32
+ Args:
33
+ tabby_shaft_word: Tabby shaft word.
34
+ threading_shaft_words: Shaft word for each warp end.
35
+ """
36
+ is_up_list = [bool(tsw & tabby_shaft_word) for tsw in threading_shaft_words]
37
+ return sum(1 for val1, val2 in itertools.pairwise(is_up_list) if val1 != val2)
38
+
39
+
40
+ def compute_tabby_shaft_word1_simple(threading: list[int]) -> int:
41
+ """Trivial tabby computation that may fail.
42
+
43
+ Handle the trivial case that all even threads are on different
44
+ shafts than all odd threads. Return 0 if that is not the case.
45
+
46
+ Args:
47
+ threading: Shaft index (0-based) for each warp end.
48
+ Negative values and adjacent duplicates are ignored.
49
+
50
+ Returns:
51
+ Tabby shaft word, or 0 if the simple case is not satified.
52
+ """
53
+ # Ignore unthreaded shafts and duplicates
54
+ pruned_threading = prune_duplicates([shaft for shaft in threading if shaft >= 0])
55
+ num_threaded_shafts = len(set(threading))
56
+ if len(threading) <= 1:
57
+ raise ValueError("Need at least 2 threaded warp ends to weave tabby.")
58
+ if num_threaded_shafts <= 1:
59
+ raise ValueError("Need at least 2 threaded shafts to weave tabby.")
60
+
61
+ threading_shaft_words = [1 << shaft for shaft in pruned_threading]
62
+ return _compute_tabby_shaft_word1_simple_impl(threading_shaft_words=threading_shaft_words)
63
+
64
+
65
+ def _compute_tabby_shaft_word1_simple_impl(threading_shaft_words: list[int]) -> int:
66
+ """Implementation of compute_tabby_shaft_word1_simple.
67
+
68
+ Handle the trivial case that all (threaded and non-duplicated)
69
+ even threads are on different shafts than all odd threads.
70
+ Return 0 if that is not the case.
71
+
72
+ Args:
73
+ threading_shaft_words: Shaft words for each warp end.
74
+ Duplicates should be removed.
75
+
76
+ Returns:
77
+ Tabby shaft word, or 0 if the simple case is not satified.
78
+ """
79
+ if len(threading_shaft_words) == 0:
80
+ return 0
81
+
82
+ tabby_shaft_word = 0
83
+ odd_ends_shaft_word = functools.reduce(operator.or_, threading_shaft_words[0::2], 0)
84
+ even_ends_shaft_word = functools.reduce(operator.or_, threading_shaft_words[1::2], 0)
85
+
86
+ if even_ends_shaft_word & odd_ends_shaft_word == 0:
87
+ min_shaft_word = min(threading_shaft_words)
88
+ tabby_shaft_word = (
89
+ even_ends_shaft_word if min_shaft_word & even_ends_shaft_word > 0 else odd_ends_shaft_word
90
+ )
91
+
92
+ return tabby_shaft_word
93
+
94
+
95
+ def compute_tabby_shaft_word1(threading: list[int]) -> int:
96
+ """Compute which shaft_set should go up for the best tabby pick 1.
97
+
98
+ Args:
99
+ threading: Shaft index (0-based) for each warp end.
100
+ Negative values and adjacent duplicates are ignored.
101
+
102
+ Returns:
103
+ shaft_word: Which shaft_set should be up for pick 1, 3, 5... of tabby.
104
+
105
+ Raises:
106
+ ValueError if there are fewer than 2 threaded warp ends,
107
+ or if the warp ends are threaded on fewer than 2 different shaft_set.
108
+
109
+ Notes:
110
+ The algorithm works by examining transitions between adjacent warp ends, as follows:
111
+
112
+ First make a pruned version of threading with no negative values
113
+ (warp ends that are not threaded) and no repeating ends.
114
+
115
+ Initialize tabby_shafts and seen_shafts to the first entry in the pruned threading.
116
+ For each shaft in the rest of the pruned threading:
117
+ If shaft is not in seen_shafts:
118
+ Add it to seen_shafts
119
+ If previous_shaft is not in tabby_shafts, put the new shaft in tabby_shafts.
120
+ Set previous_shaft to shaft
121
+ If the resulting tabby shaft word does not included the minimum threaded shaft,
122
+ invert it to provide more predictable results.
123
+
124
+ Try this several times, from different starting points in the threading,
125
+ and pick the tabby that gives the most consecutive transitions.
126
+ """
127
+ # Ignore unthreaded shafts and duplicates
128
+ pruned_threading = prune_duplicates([shaft for shaft in threading if shaft >= 0])
129
+ num_threaded_shafts = len(set(threading))
130
+ if len(threading) <= 1:
131
+ raise ValueError("Need at least 2 threaded warp ends to weave tabby.")
132
+ if num_threaded_shafts <= 1:
133
+ raise ValueError("Need at least 2 threaded shafts to weave tabby.")
134
+
135
+ threading_shaft_words = [1 << shaft for shaft in pruned_threading]
136
+
137
+ # First try the simple algorithm. It is a common case, fast to evaluate,
138
+ # and gives maximum interlacement if it works at all.
139
+ try_tabby_word = _compute_tabby_shaft_word1_simple_impl(threading_shaft_words=threading_shaft_words)
140
+ if try_tabby_word != 0:
141
+ return try_tabby_word
142
+
143
+ # Try the harder and less ideal algorithm.
144
+ min_shaft_word = min(threading_shaft_words)
145
+ max_threaded_shaft = max(pruned_threading)
146
+ all_shafts_word = (1 << (max_threaded_shaft + 1)) - 1
147
+
148
+ best_num_transitions = 0
149
+ best_tabby_shaft_word = 0
150
+
151
+ # Try starting at several different points in the threading,
152
+ # in order to get a somewhat better result.
153
+ num_threads = len(pruned_threading)
154
+ start_interval = ((num_threads - 1) // MAX_ITER_TABBY1) + 1
155
+ if num_threads < MAX_ITER_TABBY1 * 2:
156
+ # Not many threads; don't worry about limiting the number of iterations.
157
+ start_interval = 1
158
+ niter = 0
159
+ for start_index in range(0, num_threads, start_interval):
160
+ niter += 1
161
+ tabby_shaft_word = threading_shaft_words[start_index]
162
+ seen_shafts_word = threading_shaft_words[start_index]
163
+ num_seen_shafts = 1
164
+ for prev_shaft_word, shaft_word in itertools.pairwise(
165
+ itertools.chain(threading_shaft_words[start_index:], threading_shaft_words[:start_index])
166
+ ):
167
+ if shaft_word & seen_shafts_word > 0:
168
+ continue
169
+ seen_shafts_word |= shaft_word
170
+ num_seen_shafts += 1
171
+ if prev_shaft_word & tabby_shaft_word == 0:
172
+ # The previous thread's shaft word is not part of the tabby word
173
+ # so make this new thread's shaft part of the tabby word.
174
+ tabby_shaft_word |= shaft_word
175
+ if num_seen_shafts == num_threaded_shafts:
176
+ # We have seen all shafts; stop
177
+ break
178
+
179
+ # Pick the tabby word that includes the lowest numbered threaded shaft
180
+ if min_shaft_word & tabby_shaft_word == 0:
181
+ tabby_shaft_word = ~tabby_shaft_word & all_shafts_word
182
+
183
+ num_transitions = _compute_num_transitions_impl(
184
+ tabby_shaft_word=tabby_shaft_word,
185
+ threading_shaft_words=threading_shaft_words,
186
+ )
187
+ if num_transitions > best_num_transitions:
188
+ best_num_transitions = num_transitions
189
+ best_tabby_shaft_word = tabby_shaft_word
190
+ if num_transitions == len(pruned_threading) - 1:
191
+ break
192
+ return best_tabby_shaft_word
193
+
194
+
195
+ def compute_tabby_shaft_word2(tabby_shaft_word1: int, threading: list[int]) -> int:
196
+ """Compute tabby_shaft_word2 as the complement of tabby_shaft_word1.
197
+
198
+ Args:
199
+ tabby_shaft_word1: tabby shaft word computed by compute_tabby_shaft_word1.
200
+ threading: Shaft index (0-based) for each warp end.
201
+ Negative values and adjacent duplicates are ignored.
202
+
203
+ Returns:
204
+ tabby_shaft_word2: which shaft_set should be up picks 2, 4, 6... of tabby.
205
+ The complement tabby_shaft_word1, will all shaft_set beyond max_threaded_shaft_number
206
+ set to 0.
207
+
208
+ Raises:
209
+ ValueError if max_threaded_shaft_number < 2.
210
+
211
+ Notes:
212
+ Takes threading instead of max_threaded_shaft_number for two reasons:
213
+
214
+ * It proved too easy to mis-compute the value. Threading is 0-based
215
+ and it was too easy to pass in max(threading), which is 1 too small.
216
+ * We may wish to switch to using a sparse mask of threaded shafts,
217
+ lowering all unused shafts, not just those beyond the last threaded shaft.
218
+ """
219
+ if tabby_shaft_word1 < 1:
220
+ raise ValueError(f"{tabby_shaft_word1=} must be positive")
221
+ max_threaded_shaft_number = max(threading) + 1 # threading is 0-based
222
+ if max_threaded_shaft_number <= 1:
223
+ raise ValueError(f"{max_threaded_shaft_number=} must be > 1")
224
+
225
+ all_shafts_mask = (1 << max_threaded_shaft_number) - 1
226
+ tabby_shaft_word2 = ~tabby_shaft_word1 & all_shafts_mask
227
+ if tabby_shaft_word2 < 1:
228
+ raise ValueError(f"Result is invalid; check {tabby_shaft_word1=} and {threading=}")
229
+ return tabby_shaft_word2
230
+
231
+
232
+ def compute_tabby_shaft_words(threading: list[int]) -> tuple[int, int]:
233
+ """Compute tabby_shaft_words 1 and 2.
234
+
235
+ Args:
236
+ threading: Shaft index (0-based) for each warp end.
237
+ Negative values and adjacent duplicates are ignored.
238
+
239
+ Returns:
240
+ (shaft_word1, shaft_word2): Which shaft_set should be up for the two picks of tabby
241
+ (word 1 is for picks 1, 3, 5.., word 2 is for picks 2, 4, 6...).
242
+
243
+ Raises:
244
+ ValueError if there are fewer than 2 threaded warp ends,
245
+ or if the warp ends are threaded on fewer than 2 different shaft_set.
246
+ """
247
+ tabby_shaft_word1 = compute_tabby_shaft_word1(threading=threading)
248
+ tabby_shaft_word2 = compute_tabby_shaft_word2(tabby_shaft_word1=tabby_shaft_word1, threading=threading)
249
+ return (tabby_shaft_word1, tabby_shaft_word2)
@@ -38,7 +38,7 @@ class Pick:
38
38
 
39
39
  Args:
40
40
  color: Weft color, as an index into the color table.
41
- shaft_word: A bit mask, with bit 1 = shaft 0.
41
+ shaft_word: A bit mask, with bit 1 = shaft number 1 (index 0).
42
42
  The shaft is up if the bit is set.
43
43
  """
44
44
 
@@ -62,16 +62,27 @@ class ReducedPattern:
62
62
  Contains just enough information to allow loom control,
63
63
  with a simple display.
64
64
 
65
- Picks are accessed by pick number, which is 1-based.
66
- 0 indicates that nothing has been woven.
67
- Similarly for tabby picks and warp ends.
65
+ Warp ends that are not threaded on any shaft are omitted.
66
+ Warp ends that are threaded on more than one shaft are
67
+ only threaded on the lowest-numbered shaft.
68
+ Weft picks that are not treadled are omitted.
68
69
 
69
- Shaft numbers in threading are 0-based.
70
+ Values in threading and warp_colors, are 0-based indices:
71
+
72
+ * threading is a list of shaft indices (shaft index 0 is shaft number 1).
73
+ * warp_colors is a list of indices into color_table.
74
+ Likewise for the color attribute of Pick.
75
+
76
+ pick_number and end_number0/1 are 1-based (in general "number"
77
+ in the name indicates it is 1-based).
78
+ A value of 0 indicates "no such pick" or "no such end",
79
+ and is the initial state of weaving and threading,
80
+ as well as a separator gap between pattern repeats.
70
81
 
71
82
  pick_number and end_number0/1 are within one pattern repeat,
72
83
  and repeats are tracked with related attributes.
73
- tabby_pick_number is different, since repeats of tabby are not tracked
74
- (as uninteresting), the value is a "total" pick number.
84
+ However, tabby_pick_number is absolute, because repeats of tabby
85
+ are not interesting, and so are not tracked.
75
86
  """
76
87
 
77
88
  type: str = dataclasses.field(init=False, default="ReducedPattern")
@@ -473,8 +484,9 @@ def reduced_pattern_from_pattern_data(name: str, data: dtx_to_wif.PatternData) -
473
484
 
474
485
  num_ends = max(data.threading.keys())
475
486
  end_numbers = list(range(1, num_ends + 1))
487
+ # Shaft numbers in threading are 0-based
476
488
  threading = [_smallest_shaft(data.threading.get(end_number, {0})) - 1 for end_number in end_numbers]
477
- max_threaded_shaft = max(threading)
489
+ max_threaded_shaft_number = max(threading) + 1 # +1 because 1-based
478
490
 
479
491
  num_picks = max(data.liftplan.keys()) if data.liftplan else max(data.treadling.keys())
480
492
  pick_numbers = list(range(1, num_picks + 1))
@@ -493,10 +505,10 @@ def reduced_pattern_from_pattern_data(name: str, data: dtx_to_wif.PatternData) -
493
505
  if len(shaft_sets) != len(weft_colors):
494
506
  raise RuntimeError(f"{len(shaft_sets)=} != {len(weft_colors)=}\n{shaft_sets=}\n{weft_colors=}")
495
507
  try:
496
- max_shaft_raised = max(max(shaft_set) for shaft_set in shaft_sets if shaft_set)
508
+ max_raised_shaft_number = max(max(shaft_set) for shaft_set in shaft_sets if shaft_set)
497
509
  except (ValueError, TypeError):
498
510
  raise RuntimeError("No shafts are raised") from None
499
- all_shafts = set(range(1, max_shaft_raised + 1))
511
+ all_shafts = set(range(1, max_raised_shaft_number + 1))
500
512
  if data.is_rising_shed:
501
513
  shaft_words = [bitmask_from_bits(shaft_set) for shaft_set in shaft_sets]
502
514
  else:
@@ -519,7 +531,7 @@ def reduced_pattern_from_pattern_data(name: str, data: dtx_to_wif.PatternData) -
519
531
  threading=threading,
520
532
  picks=picks,
521
533
  tabby_picks=tabby_picks,
522
- num_shafts=max(max_shaft_raised, max_threaded_shaft),
534
+ num_shafts=max(max_raised_shaft_number, max_threaded_shaft_number),
523
535
  separate_weaving_repeats=len(picks) > NUM_ITEMS_FOR_REPEAT_SEPARATOR,
524
536
  separate_threading_repeats=len(threading) > NUM_ITEMS_FOR_REPEAT_SEPARATOR,
525
537
  )
@@ -445,8 +445,9 @@ class Client:
445
445
  """
446
446
  expected_seen_types = {
447
447
  "CommandDone",
448
- "CurrentPickNumber",
449
448
  "CurrentEndNumber",
449
+ "CurrentPickNumber",
450
+ "CurrentTabbyPickNumber",
450
451
  "ReducedPattern",
451
452
  "SeparateThreadingRepeats",
452
453
  "SeparateWeavingRepeats",
@@ -486,6 +487,8 @@ class Client:
486
487
  repeat_number=pattern_in_reply.pick_repeat_number,
487
488
  repeat_len=len(pattern_in_reply.picks),
488
489
  )
490
+ case "CurrentTabbyPickNumber":
491
+ assert reply.tabby_pick_number == pattern_in_reply.tabby_pick_number
489
492
  case "CurrentEndNumber":
490
493
  assert reply.end_number0 == pattern_in_reply.end_number0
491
494
  assert reply.end_number1 == pattern_in_reply.end_number1
@@ -1,6 +1,7 @@
1
1
  import asyncio
2
2
  import importlib
3
- from collections.abc import Iterable
3
+ import itertools
4
+ from collections.abc import Iterable, Sequence
4
5
 
5
6
 
6
7
  def bitmask_from_bits(bit_nums: Iterable[int]) -> int:
@@ -9,6 +10,7 @@ def bitmask_from_bits(bit_nums: Iterable[int]) -> int:
9
10
  Repeated values are ignored (naturally).
10
11
  """
11
12
  bit_set = set(bit_nums)
13
+
12
14
  return sum(1 << bit_num - 1 for bit_num in bit_set if bit_num > 0)
13
15
 
14
16
 
@@ -93,6 +95,13 @@ def get_version(package_name: str) -> str:
93
95
  return str(getattr(module, "__version__", "?"))
94
96
 
95
97
 
98
+ def prune_duplicates(data: Sequence[int]) -> list[int]:
99
+ """Prune duplicate entries in an ordered sequence."""
100
+ if len(data) == 0:
101
+ return []
102
+ return [data[0]] + [val1 for val0, val1 in itertools.pairwise(data) if val1 != val0]
103
+
104
+
96
105
  async def run_shell_command(command: str) -> str:
97
106
  """Run a shell command and return the result.
98
107
 
@@ -1,3 +1,3 @@
1
1
  # Generated by setuptools_scm
2
2
  __all__ = ["__version__"]
3
- __version__ = "1.2b2"
3
+ __version__ = "1.2b4"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: base_loom_server
3
- Version: 1.2b2
3
+ Version: 1.2b4
4
4
  Summary: Base package for web servers that control dobby multi-shaft looms
5
5
  Author-email: Russell Owen <r3owen@gmail.com>
6
6
  Project-URL: Homepage, https://pypi.org/project/base-loom-server/
@@ -0,0 +1,109 @@
1
+ import pytest
2
+
3
+ from base_loom_server.compute_tabby import (
4
+ compute_num_transitions,
5
+ compute_tabby_shaft_word1,
6
+ compute_tabby_shaft_word2,
7
+ compute_tabby_shaft_words,
8
+ )
9
+
10
+ # Dict of field name: default value
11
+ EXPECTED_DEFAULTS = dict(
12
+ pick_number=0,
13
+ pick_repeat_number=1,
14
+ end_number0=0,
15
+ end_number1=0,
16
+ end_repeat_number=1,
17
+ )
18
+
19
+
20
+ def test_known_values() -> None:
21
+ """Get the shaft set for a specified 1-based pick_number."""
22
+ # Explicit values use 1-based shafts, for readability,
23
+ # but internally the threading array uses 0-based shafts.
24
+ for threading_1based, expected_shaft_word1, expected_num_transitions in (
25
+ # Fully interlaced
26
+ ([1, 2], 0b01, 0),
27
+ ([1, 2, 3, 4, 3, 2, 1], 0b0101, 0),
28
+ ([1, 2, 3, 4, 1, 2, 3], 0b0101, 0),
29
+ ([4, 3, 2, 1, 2, 3, 4], 0b0101, 0),
30
+ ([2, 3, 4, 5, 4, 3, 2], 0b01010, 0),
31
+ ([3, 4, 5, 6, 5, 4, 3], 0b010100, 0),
32
+ ([1, 10, 3, 6], 0b0000000101, 0),
33
+ ([2, 5, 4, 9], 0b000001010, 0),
34
+ ([2, 1, 2, 3, 4, 5], 0b10101, 0),
35
+ # Some repeating warp ends; ignoring those
36
+ # the fabric is fully interlaced.
37
+ ([6, 5, 4, 3, 3, 4, 5], 0b10100, 5),
38
+ ([5, 4, 3, 2, 2, 3, 4], 0b01010, 5),
39
+ ([1, 1, 2, 2], 0b01, 1),
40
+ ([1, 1, 1, 2, 2, 3, 3, 3], 0b101, 2),
41
+ # Overshot (perfect interlacemet)
42
+ ([1, 2, 1, 2, 3, 2, 3, 4, 3, 4, 1, 4, 1], 0b0101, 0),
43
+ # Bronson lace (perfect interlacemet)
44
+ ([1, 2, 1, 2, 1, 3, 1, 3, 1, 4, 1, 4], 0b0001, 0),
45
+ # Canvas weave
46
+ ([1, 2, 2, 1, 4, 3, 3, 4], 0b0101, 5),
47
+ # Non-trivial cases. The even warp ends and odd warp ends
48
+ # are not on unique sets of shafts (after purging repeating shafts).
49
+ # These are the only test cases that exercise the
50
+ # non-simple branch of the algorithm.
51
+ ([1, 2, 3, 1, 2, 3], 0b101, 4),
52
+ ([1, 2, 4, 2, 3, 1], 0b1101, 4),
53
+ ):
54
+ threading = [shaft - 1 for shaft in threading_1based]
55
+ if expected_num_transitions == 0:
56
+ expected_num_transitions = len(threading) - 1 # noqa: PLW2901
57
+
58
+ max_shaft_number = max(threading) + 1
59
+ expected_shaft_word2 = ~expected_shaft_word1 & (2**max_shaft_number - 1)
60
+
61
+ tabby_shaft_word1 = compute_tabby_shaft_word1(threading=threading)
62
+ tabby_shaft_word2 = compute_tabby_shaft_word2(
63
+ tabby_shaft_word1=tabby_shaft_word1, threading=threading
64
+ )
65
+ num_transitions = compute_num_transitions(tabby_shaft_word1, threading=threading)
66
+
67
+ if tabby_shaft_word1 != expected_shaft_word1 or num_transitions != expected_num_transitions:
68
+ print( # noqa: T201
69
+ f"Failed for {threading_1based=}; {tabby_shaft_word1=:b}, "
70
+ f"{expected_shaft_word1=:b}, {num_transitions=}, {expected_num_transitions=}"
71
+ )
72
+
73
+ assert tabby_shaft_word1 == expected_shaft_word1
74
+ assert tabby_shaft_word2 == expected_shaft_word2
75
+ assert num_transitions == expected_num_transitions
76
+
77
+ assert expected_shaft_word1, expected_shaft_word2 == compute_tabby_shaft_words(threading)
78
+
79
+
80
+ def test_invalid_values() -> None:
81
+ # Use 0-based threading here, as readability is less important
82
+ for threading in (
83
+ # Need at least two threaded warp ends
84
+ [],
85
+ [0],
86
+ [1],
87
+ # Need at least two different threaded shafts
88
+ [0, 0],
89
+ [1, 1],
90
+ [5, 5],
91
+ ):
92
+ with pytest.raises(ValueError):
93
+ compute_tabby_shaft_word1(threading=threading)
94
+ with pytest.raises(ValueError):
95
+ compute_tabby_shaft_words(threading=threading)
96
+ if len(threading) == 0:
97
+ # No warp ends are threaded
98
+ with pytest.raises(ValueError):
99
+ compute_tabby_shaft_word2(tabby_shaft_word1=0b1, threading=threading)
100
+ else:
101
+ # Raise no shafts on tabby 1
102
+ with pytest.raises(ValueError):
103
+ compute_tabby_shaft_word2(tabby_shaft_word1=0, threading=threading)
104
+
105
+ # Raise all shafts on tabby 1, so none are raised on tabby 2
106
+ max_threaded_shaft_number = max(threading) + 1
107
+ all_shafts_up = 2**max_threaded_shaft_number - 1
108
+ with pytest.raises(ValueError):
109
+ compute_tabby_shaft_word2(tabby_shaft_word1=all_shafts_up, threading=threading)
@@ -8,6 +8,7 @@ from base_loom_server.utils import (
8
8
  compute_num_within_and_repeats,
9
9
  compute_total_num,
10
10
  get_version,
11
+ prune_duplicates,
11
12
  )
12
13
 
13
14
 
@@ -39,20 +40,6 @@ def test_bitmask_functions() -> None:
39
40
  assert bitmask == bitmask_from_bits(bits)
40
41
 
41
42
 
42
- def test_compute_total_num() -> None:
43
- for num_within, repeat_number, repeat_len in itertools.product(
44
- (-50, -33, -1, 0, 1, 33, 50), (-1, 0, 1), (1, 21, 33, 50)
45
- ):
46
- total_num = compute_total_num(num_within, repeat_number, repeat_len)
47
- assert total_num == (repeat_number - 1) * repeat_len + num_within
48
-
49
- with pytest.raises(ValueError):
50
- compute_total_num(num_within, repeat_number, 0)
51
-
52
- with pytest.raises(ValueError):
53
- compute_total_num(num_within, repeat_number, -1)
54
-
55
-
56
43
  def test_compute_num_within_and_repeats() -> None:
57
44
  for num_within, repeat_number, repeat_len in itertools.product(
58
45
  (-50, -33, -1, 0, 1, 33, 50), (-1, 0, 1), (1, 21, 33, 50)
@@ -83,6 +70,20 @@ def test_compute_num_within_and_repeats() -> None:
83
70
  compute_num_within_and_repeats(total_num, -1)
84
71
 
85
72
 
73
+ def test_compute_total_num() -> None:
74
+ for num_within, repeat_number, repeat_len in itertools.product(
75
+ (-50, -33, -1, 0, 1, 33, 50), (-1, 0, 1), (1, 21, 33, 50)
76
+ ):
77
+ total_num = compute_total_num(num_within, repeat_number, repeat_len)
78
+ assert total_num == (repeat_number - 1) * repeat_len + num_within
79
+
80
+ with pytest.raises(ValueError):
81
+ compute_total_num(num_within, repeat_number, 0)
82
+
83
+ with pytest.raises(ValueError):
84
+ compute_total_num(num_within, repeat_number, -1)
85
+
86
+
86
87
  def test_get_version() -> None:
87
88
  assert get_version("#_invalid_package_name") == "?"
88
89
 
@@ -92,3 +93,22 @@ def test_get_version() -> None:
92
93
  assert get_version("base_loom_server") == "?"
93
94
  else:
94
95
  assert get_version("base_loom_server") == getattr(version, "__version__", "?")
96
+
97
+
98
+ def test_prune_duplicates() -> None:
99
+ for data in (
100
+ [],
101
+ [1, 1, 1, 2, 3, 3, 0, 3, 2, -1, -1],
102
+ [-1, 2, 0, 1],
103
+ [0, 55, 55, 22, 22, -1, -1, 55],
104
+ ):
105
+ pruned_data = prune_duplicates(data)
106
+ # compare to a different implementation
107
+ desired_pruned_data: list[int] = []
108
+ prev_val: int | None = None
109
+ for end in data:
110
+ if end != prev_val:
111
+ prev_val = end
112
+ desired_pruned_data.append(end)
113
+
114
+ assert pruned_data == desired_pruned_data
@@ -1,183 +0,0 @@
1
- from itertools import pairwise
2
-
3
- # Maximum starting points for walking through the threading
4
- # in compute_tabby_shaft_word1. This keeps the compute time sane.
5
- MAX_ITER = 5
6
-
7
-
8
- def compute_num_transitions(tabby_shaft_word: int, threading: list[int]) -> int:
9
- """Compute the number of transitions produced by a tabby shaft word.
10
-
11
- The largest possible value is len(threading_shaft_words) - 1.
12
-
13
- Args:
14
- tabby_shaft_word: Tabby shaft word.
15
- threading: Which shaft each warp end is on.
16
- """
17
- threading_shaft_words = [1 << (shaft - 1) for shaft in threading if shaft > 0]
18
- return _basic_compute_num_transitions(
19
- tabby_shaft_word=tabby_shaft_word, threading_shaft_words=threading_shaft_words
20
- )
21
-
22
-
23
- def _basic_compute_num_transitions(tabby_shaft_word: int, threading_shaft_words: list[int]) -> int:
24
- """Implementation of compute_num_transitions.
25
-
26
- Args:
27
- tabby_shaft_word: Tabby shaft word.
28
- threading_shaft_words: Shaft word for each warp end.
29
- """
30
- is_up_list = [bool(tsw & tabby_shaft_word) for tsw in threading_shaft_words]
31
- return sum(1 for val1, val2 in pairwise(is_up_list) if val1 != val2)
32
-
33
-
34
- def compute_tabby_shaft_word1(threading: list[int]) -> int:
35
- """Compute which shaft_set should go up for the best tabby pick 1.
36
-
37
- Args:
38
- threading: List of 1-based shaft number for each warp end.
39
- Ends with shaft < 1 are ignored.
40
-
41
- Returns:
42
- shaft_word: Which shaft_set should be up for pick 1, 3, 5... of tabby.
43
-
44
- Raises:
45
- ValueError if there are fewer than 2 threaded warp ends,
46
- or if the warp ends are threaded on fewer than 2 different shaft_set.
47
-
48
- Notes:
49
- The algorithm is as follows:
50
-
51
- First make a pruned version of the threading with no 0s
52
- (warp ends that are not threaded) and no repeating ends.
53
-
54
- Now check the most common case: if all even (pruned) warp ends
55
- are threaded on a separate set of shafts than all odd warp ends,
56
- (a case that allows optimal interlacement) then return
57
- a tabby shaft word that raises every shaft in the odd warp ends set.
58
-
59
- If that fails, use a harder and less ideal algorithm:
60
- Loop through the (pruned) warp ends. For each shaft that
61
- has not been seen before, append it to a list of seen shafts.
62
- Then make a tabby shaft word that raises every other shaft
63
- of the seen shafts.
64
-
65
- Try this several times, from different starting points
66
- in the threading, and pick the tabby shaft word that gives
67
- the most consecutive transitions.
68
- """
69
- nonzero_threading = [shaft for shaft in threading if shaft > 0]
70
- num_threaded_shafts = len(set(nonzero_threading))
71
- if len(nonzero_threading) <= 1:
72
- raise ValueError("Need at least 2 threaded warp ends to weave tabby.")
73
- if num_threaded_shafts <= 1:
74
- raise ValueError("Need at least 2 threaded shafts to weave tabby.")
75
-
76
- # Prune repeating duplicate ends
77
- pruned_threading: list[int] = []
78
- prev_end = 0
79
- for end in nonzero_threading:
80
- if end != prev_end:
81
- prev_end = end
82
- pruned_threading.append(end)
83
-
84
- # Try the common case that the odd warp ends are on one set of shafts
85
- # and the even warp ends are on another set of shafts, with no overlap
86
- odd_ends_shaft_set = set(pruned_threading[::2])
87
- even_ends_shaft_set = set(pruned_threading[1::2])
88
- if even_ends_shaft_set & odd_ends_shaft_set == set():
89
- tabby_shaft_word = 0
90
- for shaft in odd_ends_shaft_set:
91
- tabby_shaft_word |= 1 << (shaft - 1)
92
- return tabby_shaft_word
93
-
94
- # Try the harder and less ideal algorithm.
95
- threading_shaft_words = [1 << (shaft - 1) for shaft in threading if shaft > 0]
96
-
97
- best_num_transitions = 0
98
- best_tabby_shaft_word = 0
99
-
100
- # Try starting at several different points in the threading,
101
- # in order to get a somewhat better result.
102
- num_threads = len(pruned_threading)
103
- start_interval = ((num_threads - 1) // MAX_ITER) + 1
104
- if num_threads < MAX_ITER * 2:
105
- # Not many threads; don't worry about limiting the number of iterations.
106
- start_interval = 1
107
- niter = 0
108
- for start_index in range(0, num_threads, start_interval):
109
- niter += 1
110
- seen_shaft_set: set[int] = set()
111
- seen_shaft_arr: list[int] = []
112
- for shaft in pruned_threading[start_index:] + pruned_threading[:start_index]:
113
- if shaft in seen_shaft_set:
114
- continue
115
- seen_shaft_set.add(shaft)
116
- seen_shaft_arr.append(shaft)
117
- if len(seen_shaft_set) == num_threaded_shafts:
118
- # We have seen all the shafts; stop
119
- break
120
-
121
- tabby_shaft_word = 0
122
- for shaft in seen_shaft_arr[::2]:
123
- tabby_shaft_word |= 1 << (shaft - 1)
124
-
125
- num_transitions = _basic_compute_num_transitions(
126
- tabby_shaft_word=tabby_shaft_word, threading_shaft_words=threading_shaft_words
127
- )
128
- if num_transitions > best_num_transitions:
129
- best_num_transitions = num_transitions
130
- best_tabby_shaft_word = tabby_shaft_word
131
- if num_transitions == len(pruned_threading) - 1:
132
- # Unlikely, since the simple case failed
133
- # but it's quick to check and we can't do better
134
- break
135
- return best_tabby_shaft_word
136
-
137
-
138
- def compute_tabby_shaft_word2(tabby_shaft_word1: int, max_threaded_shaft: int) -> int:
139
- """Compute tabby_shaft_word2 as the complement of tabby_shaft_word1.
140
-
141
- Args:
142
- tabby_shaft_word1: tabby shaft word computed by compute_tabby_shaft_word1.
143
- max_threaded_shaft: max(threading); the maximum shaft number (1-based)
144
- that has any warp strings threaded on it. All bits beyond that
145
- will be 0.
146
-
147
- Returns:
148
- tabby_shaft_word2: which shaft_set should be up picks 2, 4, 6... of tabby.
149
- The complement tabby_shaft_word1, will all shaft_set beyond max_threaded_shaft
150
- set to 0.
151
-
152
- Raises:
153
- ValueError if max_threaded_shaft < 2.
154
- """
155
- if tabby_shaft_word1 < 1:
156
- raise ValueError(f"{tabby_shaft_word1=} must be positive")
157
- if max_threaded_shaft <= 1:
158
- raise ValueError(f"{max_threaded_shaft=} must be > 1")
159
- all_shaft_set_mask = (1 << max_threaded_shaft) - 1
160
- return ~tabby_shaft_word1 & all_shaft_set_mask
161
-
162
-
163
- def compute_tabby_shaft_words(threading: list[int]) -> tuple[int, int]:
164
- """Compute tabby_shaft_words 1 and 2.
165
-
166
- Args:
167
- threading: List of 1-based shaft number for each warp end.
168
- Ends with shaft < 1 are ignored.
169
-
170
- Returns:
171
- (shaft_word1, shaft_word2): Which shaft_set should be up for the two picks of tabby
172
- (word 1 is for picks 1, 3, 5.., word 2 is for picks 2, 4, 6...).
173
-
174
- Raises:
175
- ValueError if there are fewer than 2 threaded warp ends,
176
- or if the warp ends are threaded on fewer than 2 different shaft_set.
177
- """
178
- tabby_shaft_word1 = compute_tabby_shaft_word1(threading=threading)
179
- max_threaded_shaft = max(threading)
180
- tabby_shaft_word2 = compute_tabby_shaft_word2(
181
- tabby_shaft_word1=tabby_shaft_word1, max_threaded_shaft=max_threaded_shaft
182
- )
183
- return (tabby_shaft_word1, tabby_shaft_word2)
@@ -1,98 +0,0 @@
1
- import itertools
2
-
3
- import pytest
4
-
5
- from base_loom_server.compute_tabby import (
6
- compute_num_transitions,
7
- compute_tabby_shaft_word1,
8
- compute_tabby_shaft_word2,
9
- compute_tabby_shaft_words,
10
- )
11
-
12
- # Dict of field name: default value
13
- EXPECTED_DEFAULTS = dict(
14
- pick_number=0,
15
- pick_repeat_number=1,
16
- end_number0=0,
17
- end_number1=0,
18
- end_repeat_number=1,
19
- )
20
-
21
-
22
- def test_known_values() -> None:
23
- """Get the shaft set for a specified 1-based pick_number."""
24
- for threading, expected_shaft_word1, expected_shaft_word2, expected_num_transitions in (
25
- # Fully interlaced
26
- ([1, 2], 0b01, 0b10, 0),
27
- ([1, 2, 3, 4, 3, 2, 1], 0b0101, 0b1010, 0),
28
- ([1, 2, 3, 4, 1, 2, 3], 0b0101, 0b1010, 0),
29
- ([4, 3, 2, 1, 2, 3, 4], 0b1010, 0b0101, 0),
30
- ([2, 3, 4, 5, 4, 3, 2], 0b01010, 0b10101, 0),
31
- ([3, 4, 5, 6, 5, 4, 3], 0b010100, 0b101011, 0),
32
- ([1, 10, 3, 6], 0b0000000101, 0b1111111010, 0),
33
- ([2, 5, 4, 9], 0b000001010, 0b111110101, 0),
34
- ([2, 1, 2, 3, 4, 5], 0b01010, 0b10101, 0),
35
- # Some repeating warp ends; ignoring those
36
- # the fabric is fully interlaced.
37
- ([6, 5, 4, 3, 3, 4, 5], 0b101000, 0b010111, 5),
38
- ([5, 4, 3, 2, 2, 3, 4], 0b10100, 0b01011, 5),
39
- ([1, 1, 2, 2], 0b01, 0b10, 1),
40
- ([1, 1, 1, 2, 2, 3, 3, 3], 0b101, 0b010, 2),
41
- # Overshot (perfect interlacemet)
42
- ([1, 2, 1, 2, 3, 2, 3, 4, 3, 4, 1, 4, 1], 0b0101, 0b1010, 0),
43
- # Bronson lace (perfect interlacemet)
44
- ([1, 2, 1, 2, 1, 3, 1, 3, 1, 4, 1, 4], 0b0001, 0b1110, 0),
45
- # Canvas weave
46
- ([1, 2, 2, 1, 4, 3, 3, 4], 0b0101, 0b1010, 5),
47
- # Non-trivial cases. The even warp ends and odd warp ends
48
- # are not on unique sets of shafts (after purging repeating shafts).
49
- # These are the only test cases that exercise the
50
- # non-simple branch of the algorithm.
51
- ([1, 2, 3, 1, 2, 3], 0b101, 0b010, 4),
52
- ([1, 2, 4, 2, 3, 1], 0b1001, 0b0110, 4),
53
- ):
54
- if expected_num_transitions == 0:
55
- expected_num_transitions = len(threading) - 1 # noqa: PLW2901
56
-
57
- tabby_shaft_word1 = compute_tabby_shaft_word1(threading=threading)
58
-
59
- max_threaded_shaft = max(threading)
60
- tabby_shaft_word2 = compute_tabby_shaft_word2(
61
- tabby_shaft_word1=tabby_shaft_word1, max_threaded_shaft=max_threaded_shaft
62
- )
63
-
64
- num_transitions = compute_num_transitions(tabby_shaft_word1, threading=threading)
65
-
66
- assert tabby_shaft_word1 == expected_shaft_word1
67
- assert tabby_shaft_word2 == expected_shaft_word2
68
- assert num_transitions == expected_num_transitions
69
-
70
- assert expected_shaft_word1, expected_shaft_word2 == compute_tabby_shaft_words(threading)
71
-
72
-
73
- def test_invalid_values() -> None:
74
- for threading in (
75
- # Need at least two threaded warp ends
76
- [],
77
- [0],
78
- [1],
79
- [0, 0],
80
- [0, 1],
81
- [1, 0],
82
- # Need at least two different threaded shafts
83
- [1, 1],
84
- [5, 5],
85
- ):
86
- with pytest.raises(ValueError):
87
- compute_tabby_shaft_word1(threading=threading)
88
- with pytest.raises(ValueError):
89
- compute_tabby_shaft_words(threading=threading)
90
-
91
- for tabby_shaft_word1, max_threaded_shaft in itertools.product((-1, 0, 1, 2), (-1, 0, 1, 2, 3)):
92
- if tabby_shaft_word1 > 0 and max_threaded_shaft > 1:
93
- # A valid combination
94
- continue
95
- with pytest.raises(ValueError):
96
- compute_tabby_shaft_word2(
97
- tabby_shaft_word1=tabby_shaft_word1, max_threaded_shaft=max_threaded_shaft
98
- )