Imagining a 2-Hour, 4-Week Psalter
- Overview
- Comparing Major Psalters
- Analyzing Psalm Lengths
- Choosing Divisions
- Grouping Psalms into Hours
- Filling the Calendar
- Analyzing the Results
- Next Steps and Future Work
- Notes and References
Overview
I have always been attracted to the idea of praying all 150 Psalms in one week. I have experimented with praying two such Psalters. First, the Anglican Breviary, which is simply an English translation of the 1911 Divino Afflatu Divine Office, using Pope St. Pius X’s 1-week, but very efficient Psalter. Later, the 1962 Monastic Diurnal (with supplemental resources for Matins), which still uses St. Benedict’s arrangement from the sixth century. Both proved to be too long and arduous for a simple and busy layman such as myself.
Most recently, I’ve transitioned to using the 4-volume Liturgy of the Hours. Despite disliking it more than both of the two previous versions of the Divine Office that I’ve used, it’s much shorter, and much more manageable. But due to some combination of busy-ness and a lack of discipline, I’ve only managed to be consistent with Lauds and Vespers, which leaves me with a 4-week cycle which does not even include the entire Psalter.
This all led to the question: Is a 4-week, 150-Psalm Psalter possible if we restrict ourselves to only Lauds and Vespers? Ideally, this arrangement would allow us to reuse the existing antiphons from the LotH, meaning that we’d only be able to pray 3 Psalms per hour.
Simple math says yes:
- 3 Psalms per hour * 2 hours per day =
- 6 Psalms per day * 7 days per week =
- 42 Psalms per week * 4 weeks per cycle =
- 168 Psalms
If we eliminate all of the canticles, it allows us to pray every Psalm, and gives us 18 slots to fill with divisions for the longer Psalms. This is many fewer division slots than the current LotH, so we will have to be strategic in how we use them.
Our goal is to create a 4-week, 2-hour Psalter that:
- Uses all 150 Psalms with minimal repetition (likely no repetition at all)
- Selects Psalm divisions that produce the greatest uniformity in length
- Arranges the Psalms in a way that conforms as much as possible to traditional Psalters
Comparing Major Psalters
Our first step is to review existing Psalters. For simplicity, I’ve chosen 3: the Monastic Psalter, Pope St. Pius X’s Psalter, and the Liturgy of the Hours’ Psalter. Hovering any of the Psalms in the tables highlights all of the instances of that Psalm in all the tables. Two notes about this:
- Hovering highlights all instances of the Psalm, including divisions.
- It is possible to switch between Greek and Masoretic numbering. But the highlighting is based on the Greek numbering. So, for example, hovering over an instance of Psalm 9 in the Masoretic numbering scheme will also highlight instances of Psalm 10, since Psalm 9 and 10 in the Masoretic numbering scheme are both the same Psalm in the Greek numbering scheme (Psalm 9).
Monastic (c. 540)
| Hour | Sunday | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday |
|---|---|---|---|---|---|---|---|
| Matins |
3
94 20 21 22 23 24 25 26 27 28 29 30 31 |
3
94 32 33 34 36a 36b 37 38 39 40 41 43 44 |
3
94 45 46 47 48 49 51 52 53 54 55 57 58 |
3
94 59 60 61 65 67a 67b 68a 68b 69 70 71 72 |
3
94 73 74 76 77a 77b 78 79 80 81 82 83 84 |
3
94 85 86 86 88a 88b 92 95 96 97 98 99 100 |
3
94 101 102 103a 103b 104a 104b 105a 105b 106a 106b 107 108 |
| Lauds |
66
50 117 62 Dn 3 148 149 150 |
66
50 5 35 Is 12 148 149 150 |
66
50 42 56 Is 38 148 149 150 |
66
50 63 64 1 Sm 2 148 149 150 |
66
50 87 89 Ex 15 148 149 150 |
66
50 75 91 Hb 3 148 149 150 |
66
50 141 142 Dt 32 148 149 150 |
| Prime |
118
118 118 118 |
1
2 6 |
7
8 9a |
9b
10 11 |
12
13 14 |
15
16 17a |
17b
18 19 |
| Terce |
118
118 118 |
118
118 118 |
119
120 121 |
119
120 121 |
119
120 121 |
119
120 121 |
119
120 121 |
| Sext |
118
118 118 |
118
118 118 |
122
123 124 |
122
123 124 |
122
123 124 |
122
123 124 |
122
123 124 |
| None |
118
118 118 |
118
118 118 |
125
126 127 |
125
126 127 |
125
126 127 |
125
126 127 |
125
126 127 |
| Vespers |
109
110 111 112 |
113
114 115 116 128 |
129
130 131 132 |
134
135 136 137 |
138a
138b 139 140 |
141
143a 143b 144a |
144b
145 146 147 |
| Compline |
4
90 133 |
4
90 133 |
4
90 133 |
4
90 133 |
4
90 133 |
4
90 133 |
4
90 133 |
Pope St. Pius X (1911)
| Hour | Sunday | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday |
|---|---|---|---|---|---|---|---|
| Matins |
1
2 3 8 9a 9a 9b 9b 10 |
13
14 16 17a 17b 17c 19 20 29 |
34a
34b 34c 36a 36b 36c 37a 37b 38 |
44a
44b 45 47 48a 48b 49a 49b 50 |
61
65a 65b 67a 67b 67c 68a 68b 68c |
77a
77b 77c 77d 77e 77f 78 80 82 |
104a
104b 104c 105a 105b 105c 106a 106b 106c |
| Lauds |
92
99 62 Dn 3a 148 |
46
5 28 1 Chr 29 116 |
95
42 66 Tb 13 134 |
96
64 100 Jdt 16 145 |
97
89 35 Jer 31 146 |
98
142 84 Is 45 147 |
149
91 63 Eccl 36 150 |
| Prime |
117
118 118 |
23
18 18 |
24
24 24 |
25
51 52 |
22
71 71 |
21
21 21 |
93
93 107 |
| Terce |
118
118 118 |
26
26 27 |
39
39 39 |
53
54 54 |
72
72 72 |
79
79 81 |
101
101 101 |
| Sext |
118
118 118 |
30
30 30 |
40
41 41 |
55
56 57 |
73
73 73 |
83
83 86 |
103
103 103 |
| None |
118
118 118 |
31
32 32 |
43
43 43 |
58
58 59 |
74
75 75 |
88
88 88 |
108
108 108 |
| Vespers |
109
110 111 112 113 |
114
115 119 120 121 |
122
123 124 125 126 |
127
128 129 130 131 |
132
135 135 136 137 |
138
138 139 140 141 |
143
143 144 144 144 |
| Compline |
4
90 133 |
6
7 7 |
11
12 15 |
33
33 60 |
69
70 70 |
76
76 85 |
87
102 102 |
Liturgy of the Hours (1971)
| Hour | Sunday | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday |
|---|---|---|---|---|---|---|---|
| Invitatory | |||||||
| All Weeks | 94 | ||||||
| Office of Readings (Matins) | |||||||
| Week 1 | 1 2 3 | 6 9a 9a | 9b 9b 11 | 17a 17b 17c | 17d 17e 17f | 34a 34b 34c | 130 131a 131b |
| Week 2 | 103a 103b 103c | 30a 30b 30c | 36a 36b 36c | 38a 38b 51 | 43a 43b 43c | 37a 37b 37c | 135a 135b 135c |
| Week 3 | 144a 144b 144c | 49a 49b 49c | 67a 67b 67c | 88a 88b 88c | 88d 88e 89 | 68a 68b 68c | 106a 106b 106c |
| Week 4 | 23 65 65 | 72a 72b 72c | 101a 101b 101c | 102a 102b 102c | 43a 43b 43c | 54a 54b 54c | 49a 49b 49c |
| Morning Prayer (Lauds) | |||||||
| Week 1 |
62
Dn 3a 149 |
5
1 Chr 29 28 |
23
Tb 13 32 |
35
Jdt 16 46 |
56
Jer 31 47 |
50
Is 45 99 |
118s
Ex 15 116 |
| Week 2 |
117
Dn 3b 150 |
41
Sir 36 18a |
42
Is 38 64 |
76
1 Sm 2 96 |
79
Is 12 80 |
50
Hb 3 147 |
91
Dt 32 8 |
| Week 3 |
92
Dn 3a 148 |
83
Is 2 95 |
84
Is 26 66 |
85
Is 33 97 |
86
Is 40 98 |
50
Jer 14 99 |
118s
Wis 9 116 |
| Week 4 |
117
Dn 3b 150 |
89
Is 42 134 |
100
Dn 3 143 |
107
Is 61-62 145 |
142
Is 66 146 |
50
Tb 13 147 |
91
Ez 36 8 |
| Midday (Terce, Sext or None) | |||||||
| Week 1 | 117a 117b 117c | 18b 7a 7b | 118a 12 13 | 118b 16a 16b | 118c 24a 24b | 118d 25 27 | 118e 33a 33b |
| Week 2 | 22 75a 75b | 118f 39a 39b | 118g 52 53 | 118h 54a 54b | 118i 55 56 | 118j 58 59 | 118k 60 63 |
| Week 3 | 117a 117b 117c | 118l 70a 70b | 118m 73a 73b | 118n 69 74 | 118o 78 79 | 21a 21b 21c | 118p 33a 33b |
| Week 4 | 22 75a 75b | 118q 81 119 | 118r 87a 87b | 118s 93a 93b | 118t 127 128 | 118u 132 139 | 118v 44a 44b |
| Evening Prayer (Vespers) | |||||||
| Week 1 |
109
113a
Rev 19 |
10
14
Eph 1 |
19
20
Rev 4 |
26a
26b
Col 1 |
29
31
Rev 11-12 |
40
45
Rev 15 |
118n
15
Phil 2 |
| Week 2 |
109
113b
Rev 19 |
44a
44b
Eph 1 |
48a
48b
Rev 4 |
61
66
Col 1 |
71a
71b
Rev 11-12 |
114
120
Rev 15 |
112
115
Phil 2 |
| Week 3 |
109
110
Rev 19 |
122
123
Eph 1 |
124
130
Rev 4 |
125
126
Col 1 |
131a
131b
Rev 11-12 |
134a
134b
Rev 15 |
121
129
Phil 2 |
| Week 4 |
109
111
Rev 19 |
135a
135b
Eph 1 |
136
137
Rev 4 |
138a
138b
Col 1 |
143a
143b
Rev 11-12 |
144a
144b
Rev 15 |
140
141
Phil 2 |
| Night Prayer (Compline) | |||||||
| All Weeks | 90 | 85 | 142 | 30 129 | 15 | 87 | 4 133 |
Analyzing Psalm Lengths
The next step is to analyze the lengths of all the Psalms so that we can select which Psalms we want to divide, and how to divide them. For this (and for the rest of this project), I have used the Abbey Psalms and Canticles. Not because I have any attachment to or preference for this translation, but rather because this project was invisioned as an alternative Psalter for the Liturgy of the Hours, and starting in a year from the time this project began, this translation was going to begin being used with the new edition of the Liturgy of the Hours (2027).
Below are all the Psalms, sorted by the number of Verses.
| Psalm | Verses | Psalm | Verses | Psalm | Verses | Psalm | Verses | Psalm | Verses | Psalm | Verses |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 119 | 176 | 50 | 23 | 132 | 18 | 76 | 13 | 111 | 10 | 142 | 8 |
| 78 | 72 | 74 | 23 | 81 | 17 | 79 | 13 | 112 | 10 | 11 | 7 |
| 89 | 53 | 94 | 23 | 86 | 17 | 84 | 13 | 141 | 10 | 14 | 7 |
| 18 | 51 | 25 | 22 | 90 | 17 | 96 | 13 | 146 | 10 | 53 | 7 |
| 106 | 48 | 33 | 22 | 91 | 16 | 2 | 12 | 3 | 9 | 87 | 7 |
| 105 | 45 | 103 | 22 | 92 | 16 | 26 | 12 | 4 | 9 | 110 | 7 |
| 107 | 43 | 9 | 21 | 17 | 15 | 42 | 12 | 12 | 9 | 120 | 7 |
| 37 | 40 | 49 | 21 | 19 | 15 | 46 | 12 | 28 | 9 | 1 | 6 |
| 69 | 37 | 51 | 21 | 48 | 15 | 57 | 12 | 54 | 9 | 13 | 6 |
| 68 | 36 | 77 | 21 | 144 | 15 | 58 | 12 | 61 | 9 | 23 | 6 |
| 104 | 35 | 135 | 21 | 21 | 14 | 63 | 12 | 98 | 9 | 70 | 6 |
| 22 | 32 | 145 | 21 | 27 | 14 | 97 | 12 | 99 | 9 | 126 | 6 |
| 109 | 31 | 66 | 20 | 39 | 14 | 143 | 12 | 113 | 9 | 128 | 6 |
| 102 | 29 | 72 | 20 | 41 | 14 | 6 | 11 | 122 | 9 | 150 | 6 |
| 118 | 29 | 80 | 20 | 56 | 14 | 16 | 11 | 137 | 9 | 15 | 5 |
| 35 | 28 | 147 | 20 | 60 | 14 | 29 | 11 | 149 | 9 | 43 | 5 |
| 73 | 28 | 83 | 19 | 65 | 14 | 32 | 11 | 67 | 8 | 93 | 5 |
| 44 | 27 | 88 | 19 | 85 | 14 | 52 | 11 | 82 | 8 | 100 | 5 |
| 136 | 26 | 116 | 19 | 108 | 14 | 64 | 11 | 101 | 8 | 125 | 5 |
| 31 | 25 | 7 | 18 | 140 | 14 | 75 | 11 | 114 | 8 | 127 | 5 |
| 55 | 24 | 10 | 18 | 148 | 14 | 95 | 11 | 121 | 8 | 123 | 4 |
| 71 | 24 | 40 | 18 | 5 | 13 | 8 | 10 | 124 | 8 | 131 | 3 |
| 139 | 24 | 45 | 18 | 30 | 13 | 20 | 10 | 129 | 8 | 133 | 3 |
| 34 | 23 | 59 | 18 | 36 | 13 | 24 | 10 | 130 | 8 | 134 | 3 |
| 38 | 23 | 115 | 18 | 62 | 13 | 47 | 10 | 138 | 8 | 117 | 2 |
We can look at some charts to help us visualize. First, the length of all the Psalms in order.
psalmNumbers = string(1:150);
numVerses = ...
[ 6, 12, 9, 9, 13, 11, 18, 10, 21, 18, 7, 9, 6, 7, 5, ...
11, 15, 51, 15, 10, 14, 32, 6, 10, 22, 12, 14, 9, 11, 13, ...
25, 11, 22, 23, 28, 13, 40, 23, 14, 18, 14, 12, 5, 27, 18, ...
12, 10, 15, 21, 23, 21, 11, 7, 9, 24, 14, 12, 12, 18, 14, ...
9, 13, 12, 11, 14, 20, 8, 36, 37, 6, 24, 20, 28, 23, 11, ...
13, 21, 72, 13, 20, 17, 8, 19, 13, 14, 17, 7, 19, 53, 17, ...
16, 16, 5, 23, 11, 13, 12, 9, 9, 5, 8, 29, 22, 35, 45, ...
48, 43, 14, 31, 7, 10, 10, 9, 8, 18, 19, 2, 29, 176, 7, ...
8, 9, 4, 8, 5, 6, 5, 6, 8, 8, 3, 18, 3, 3, 21, ...
26, 9, 8, 24, 14, 10, 8, 12, 15, 21, 10, 20, 14, 9, 6];
bar(psalmNumbers,numVerses,1,FaceColor='flat',EdgeColor='black',LineWidth=1);
xlabel('Psalm Number');
ylabel('Number of Verses');
title('Verses Per Psalm');
Next, we can sort them by length. Here, we add a bar (purple) for the average Psalm length. The average is ~16.8 verses.
avgPsalmLength = round(mean(numVerses));
numVersesWithAvg = [numVerses avgPsalmLength];
psalmNumbersWithAvg = [psalmNumbers 'average'];
[sortedNumVerses, sortIdx] = sort(numVersesWithAvg,'descend');
sortedPsalmNumbers = psalmNumbersWithAvg(sortIdx);
idxOfAvg = find(sortedPsalmNumbers=='average');
b = bar(sortedPsalmNumbers,sortedNumVerses,1,FaceColor='flat',EdgeColor='black',LineWidth=1);
b.CData(idxOfAvg,:) = [0.5 0 0.5];
xlabel('Psalm Number');
ylabel('Number of Verses');
title('Verses Per Psalm -- Sorted');
This is too many bars to be useful, so let’s zoom in on the 20 longest Psalms. The average is also included here, although there are many more than 20 Psalms longer than the average length.
n = 20;
topPsalmNumbers = sortedPsalmNumbers(1:n);
topNumVerses = sortedNumVerses(1:n);
topPsalmNumbersWithAvg = [topPsalmNumbers 'average'];
topNumVersesWithAvg = [topNumVerses avgPsalmLength];
b = bar(topPsalmNumbersWithAvg,topNumVersesWithAvg,1,FaceColor='flat',EdgeColor='black',LineWidth=1);
b.CData(end,:) = [0.5 0 0.5];
xlabel('Psalm Number');
ylabel('Number of Verses');
ylim([0 200]);
title(['Verses Per Psalm -- Sorted, Top ' num2str(n)]);
text(1:n+1,topNumVersesWithAvg,num2str(topNumVersesWithAvg'),'vert','bottom','horiz','center');
Choosing Divisions
Eighteen extra Psalm slots is not very much, so we want to use them as effectively as possible. It turns out that finding a good way to use them is not that hard. Some quick back-of-napkin math shows that after our divisions, our longest Psalm or Psalm division will be <35.
The first order of business is Psalm 119, which has 176 verses. This is a special Psalm, since it is a 22-part acrostic poem, with each part consisting of 8 verses. We do not want to split the sections, so we’ll have to break up the Psalm in chunks of 8. The Monastic Psalter and the Liturgy of the Hours breaks it into 22 divisions of 8 verses, and the Pope St. Pius X Psalter breaks it into 11 divisions of 16 verses. This is too many divisions for us, since we have rather limited real estate to work with. I opted for 7 divisions: 1 division with 32 verses and 6 with 24. This has the nice result of allowing us to pray the Psalm throughout an entire week. So for Psalm 119, we use an additional 6 Psalm slots.
The next Psalm is 78, with 72 verses. Two divisions of 36 verses are a little long, so we’ll divide it into 3 parts, using up 2 more Psalm slots.
Having used 8 slots, we have 10 left. The next longest Psalm is 89, which has 53 verses. So from here, we can split the next 9 longest Psalms into 2: 89, 18, 106, 105, 107, 37, 69, 68, 104. The next two longest Psalms are 22 (32 verses) and 109 (31 verses). Because Psalm 22 is such a beautiful and important Psalm, I’ve elected to keep it intact, and use the last Psalm slot to split Psalm 109.
The following table shows all of our divisions, and the resulting lengths. Note that the divisions are not always even. An effort was made to review the contents of the Psalm and divide them in a way that respects the structure and flow of the Psalm. Exactly how we divide them does not matter too much, since, as we will discuss later, we will want to force all of these divided Psalms to be prayed back-to-back in the same hour (with the exception of Psalm 119). So lopsided divisions will not affect the length of whole hours later on.
| Psalm | Verses | Psalm | Verses | Psalm | Verses | Psalm | Verses | Psalm | Verses | Psalm | Verses |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 119a | 32 | 119f | 24 | 89a | 19 | 106b | 25 | 37a | 22 | 68b | 17 |
| 119b | 24 | 119g | 24 | 89b | 34 | 105a | 22 | 37b | 18 | 104a | 23 |
| 119c | 24 | 78a | 31 | 18a | 25 | 105b | 23 | 69a | 18 | 104b | 12 |
| 119d | 24 | 78b | 24 | 18b | 26 | 107a | 22 | 69b | 19 | 109a | 20 |
| 119e | 24 | 78c | 17 | 106a | 23 | 107b | 21 | 68a | 19 | 109b | 11 |
Below, we can see the original Top 20 Psalms, but now what they look like divided. As we can see, the average Psalm length is now ~15.0 verses.
topPsalmNumbersWithDivisions = ...
["119a" "119b" "119c" "119d" "119e" "119f" "119g" "78a" "78b" "78c" ...
"89a" "89b" "18a" "18b" "106a" "106b" "105a" "105b" "107a" "107b" ...
"37a" "37b" "69a" "69b" "68a" "68b" "104a" "104b" "22" "109a" ...
"109b" "102" "118" "35" "73" "44" "136" "31" "average"];
topPsalmNumVersesWithDivisions = ...
[32 24 24 24 24 24 24 31 24 17 19 34 25 26 23 25 22 23 22 ...
21 22 18 18 19 19 17 23 12 32 20 11 29 29 28 28 27 26 25 15];
[sortedTopPsalmsNumVersesWithDivisions, sortIdx] = ...
sort(topPsalmNumVersesWithDivisions,'descend');
sortedTopPsalmNumbersWithDivisions = topPsalmNumbersWithDivisions(sortIdx);
idxOfAvg = find(sortedTopPsalmNumbersWithDivisions=='average');
b = bar(sortedTopPsalmNumbersWithDivisions,sortedTopPsalmsNumVersesWithDivisions, ...
1,FaceColor='flat',EdgeColor='black',LineWidth=1);
b.CData(idxOfAvg,:) = [0.5 0 0.5];
xlabel('Psalm Number');
ylabel('Number of Verses');
title(['Verses Per Psalm -- Sorted, Top 20, with Divisions']);
text(1:length(sortedTopPsalmsNumVersesWithDivisions),...
sortedTopPsalmsNumVersesWithDivisions,...
num2str(sortedTopPsalmsNumVersesWithDivisions'), ...
'vert','bottom','horiz','center');
Now we can look at what the list of all Psalms now looks like. This next table shows all the Psalms, divided as specified, and sorted by length:
| Psalm | Verses | Psalm | Verses | Psalm | Verses | Psalm | Verses | Psalm | Verses | Psalm | Verses |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 89b | 34 | 38 | 23 | 37b | 18 | 108 | 14 | 75 | 11 | 129 | 8 |
| 119a | 32 | 50 | 23 | 69a | 18 | 140 | 14 | 95 | 11 | 130 | 8 |
| 22 | 32 | 74 | 23 | 7 | 18 | 148 | 14 | 8 | 10 | 138 | 8 |
| 78a | 31 | 94 | 23 | 10 | 18 | 5 | 13 | 20 | 10 | 142 | 8 |
| 102 | 29 | 105a | 22 | 40 | 18 | 30 | 13 | 24 | 10 | 11 | 7 |
| 118 | 29 | 107a | 22 | 45 | 18 | 36 | 13 | 47 | 10 | 14 | 7 |
| 35 | 28 | 37a | 22 | 59 | 18 | 62 | 13 | 111 | 10 | 53 | 7 |
| 73 | 28 | 25 | 22 | 115 | 18 | 76 | 13 | 112 | 10 | 87 | 7 |
| 44 | 27 | 33 | 22 | 132 | 18 | 79 | 13 | 141 | 10 | 110 | 7 |
| 18b | 26 | 103 | 22 | 78c | 17 | 84 | 13 | 146 | 10 | 120 | 7 |
| 136 | 26 | 107b | 21 | 68b | 17 | 96 | 13 | 3 | 9 | 1 | 6 |
| 18a | 25 | 9 | 21 | 81 | 17 | 104b | 12 | 4 | 9 | 13 | 6 |
| 106b | 25 | 49 | 21 | 86 | 17 | 2 | 12 | 12 | 9 | 23 | 6 |
| 31 | 25 | 51 | 21 | 90 | 17 | 26 | 12 | 28 | 9 | 70 | 6 |
| 119b | 24 | 77 | 21 | 91 | 16 | 42 | 12 | 54 | 9 | 126 | 6 |
| 119c | 24 | 135 | 21 | 92 | 16 | 46 | 12 | 61 | 9 | 128 | 6 |
| 119d | 24 | 145 | 21 | 17 | 15 | 57 | 12 | 98 | 9 | 150 | 6 |
| 119e | 24 | 109a | 20 | 19 | 15 | 58 | 12 | 99 | 9 | 15 | 5 |
| 119f | 24 | 66 | 20 | 48 | 15 | 63 | 12 | 113 | 9 | 43 | 5 |
| 119g | 24 | 72 | 20 | 144 | 15 | 97 | 12 | 122 | 9 | 93 | 5 |
| 78b | 24 | 80 | 20 | 21 | 14 | 143 | 12 | 137 | 9 | 100 | 5 |
| 55 | 24 | 147 | 20 | 27 | 14 | 109b | 11 | 149 | 9 | 125 | 5 |
| 71 | 24 | 89a | 19 | 39 | 14 | 6 | 11 | 67 | 8 | 127 | 5 |
| 139 | 24 | 69b | 19 | 41 | 14 | 16 | 11 | 82 | 8 | 123 | 4 |
| 106a | 23 | 68a | 19 | 56 | 14 | 29 | 11 | 101 | 8 | 131 | 3 |
| 105b | 23 | 83 | 19 | 60 | 14 | 32 | 11 | 114 | 8 | 133 | 3 |
| 104a | 23 | 88 | 19 | 65 | 14 | 52 | 11 | 121 | 8 | 134 | 3 |
| 34 | 23 | 116 | 19 | 85 | 14 | 64 | 11 | 124 | 8 | 117 | 2 |
And finally, we can visualize all of these Psalms and divisions in one big chart:
allPsalmNumbers = ...
["89b" "119a" "22" "78a" "102" "118" "35" "73" "44" "18b" "136" ...
"18a" "106b" "31" "119b" "119c" "119d" "119e" "119f" "119g" "78b" ...
"55" "71" "139" "106a" "105b" "104a" "34" "38" "50" "74" "94" "105a" ...
"107a" "37a" "25" "33" "103" "107b" "9" "49" "51" "77" "135" "145" ...
"109a" "66" "72" "80" "147" "89a" "69b" "68a" "83" "88" "116" "37b" ...
"69a" "7" "10" "40" "45" "59" "115" "132" "78c" "68b" "81" "86" "90" ...
"91" "92" "average" "17" "19" "48" "144" "21" "27" "39" "41" "56" ...
"60" "65" "85" "108" "140" "148" "5" "30" "36" "62" "76" "79" "84" ...
"96" "104b" "2" "26" "42" "46" "57" "58" "63" "97" "143" "109b" "6" ...
"16" "29" "32" "52" "64" "75" "95" "8" "20" "24" "47" "111" "112" ...
"141" "146" "3" "4" "12" "28" "54" "61" "98" "99" "113" "122" "137" ...
"149" "67" "82" "101" "114" "121" "124" "129" "130" "138" "142" "11" ...
"14" "53" "87" "110" "120" "1" "13" "23" "70" "126" "128" "150" ...
"15" "43" "93" "100" "125" "127" "123" "131" "133" "134" "117"];
allPsalmNumVerses = ...
[34 32 32 31 29 29 28 28 27 26 26 25 25 25 24 24 24 24 24 24 24 24 ...
24 24 23 23 23 23 23 23 23 23 22 22 22 22 22 22 21 21 21 21 21 21 ...
21 20 20 20 20 20 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 17 ...
17 17 17 17 16 16 15 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 ...
13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 11 11 11 11 ...
11 11 11 11 11 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 8 8 ...
8 8 8 8 8 8 8 8 7 7 7 7 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 4 3 3 3 2];
idxOfAvg = find(allPsalmNumbers=='average');
b = bar(allPsalmNumbers,allPsalmNumVerses,1, ...
FaceColor='flat',EdgeColor='black',LineWidth=1);
b.CData(idxOfAvg,:) = [0.5 0 0.5];
xlabel('Psalm Number');
ylabel('Number of Verses');
title('Verses Per Psalm -- Sorted, with Divisions');
Grouping Psalms into Hours
At first, I had envisioned manually grouping Psalms into triples (one triple per hour). But as I thought about it, I realized that it would be tedious to get good results by hand.
At its core, this is an optimization problem. We have constraints and goals, and since we also have a way to measure how well a solution meets its goals, we can write a cost function.
Goals and Approach
To start out, we can enumerate some of our most constraints, those which define the problem:
- Three Psalms are assigned to each group (or “triple”, or “bucket”).
- Each Psalm is used exactly once.
We can impose additional constraints in order to form a solution to our liking:
- All Psalm divisions must be grouped together, except:
- Each division of Psalm 119 must be in its own group.
- Triples should be no fewer than
average-15verses and no more thanaverage+15verses.
All the divided Psalms except for 119 have 3 or fewer parts, so constraint (3) can be accomplished. And we divided 119 into 7 groups, with the intention that each division be prayed on a different day for 7 days in a row.
In addition to constraints, we have some goals. For these, we very likely can’t accomplish them perfectly, but we can program our cost function so that we get the best solution possible. For our goals, we say that we are looking for a grouping of Psalms such that:
- The length of each triple (total number of verses) is as close to the average as possible (~45 verses).
- Wherever possible, we prefer buckets with sequential Psalms.
Finally, we can specify if there are some triples that we pre-define as requirements. To begin, I’ve selected a few, largely based on the Psalm groupings of the Monastic Psalter (and one triple which is reiteration of Constraint #3):
- 120, 121, 122
- 123, 124, 125
- 126, 127, 128
- 110, 111, 112
- 78a, 78b, 78c
Based on all of these constraints and goals, we can write a program which produces an optimal solution. We can then review the solution, and decide what changes we want to make, such as giving different weights to our goals, or loosening or tightening our constraints.
Implementation and First Attempt
Since I have a Computer Science background and not a math background, I had no idea how to begin programming for an optimization problem. Fortunately, ChatGPT was very helpful in pointing me in the right direction. Below is the first (correct) iteration of the implemention, which produces a good result. Here, I use MATLAB since I have access to all of its toolboxes, including the Optimization Toolbox.
N = 168;
psalm_names = ...
["89b" "119a" "22" "78a" "102" "118" "35" "73" "44" "18b" "136" ...
"18a" "106b" "31" "119b" "119c" "119d" "119e" "119f" "119g" "78b" ...
"55" "71" "139" "106a" "105b" "104a" "34" "38" "50" "74" "94" "105a" ...
"107a" "37a" "25" "33" "103" "107b" "9" "49" "51" "77" "135" "145" ...
"109a" "66" "72" "80" "147" "89a" "69b" "68a" "83" "88" "116" "37b" ...
"69a" "7" "10" "40" "45" "59" "115" "132" "78c" "68b" "81" "86" "90" ...
"91" "92" "17" "19" "48" "144" "21" "27" "39" "41" "56" ...
"60" "65" "85" "108" "140" "148" "5" "30" "36" "62" "76" "79" "84" ...
"96" "104b" "2" "26" "42" "46" "57" "58" "63" "97" "143" "109b" "6" ...
"16" "29" "32" "52" "64" "75" "95" "8" "20" "24" "47" "111" "112" ...
"141" "146" "3" "4" "12" "28" "54" "61" "98" "99" "113" "122" "137" ...
"149" "67" "82" "101" "114" "121" "124" "129" "130" "138" "142" "11" ...
"14" "53" "87" "110" "120" "1" "13" "23" "70" "126" "128" "150" ...
"15" "43" "93" "100" "125" "127" "123" "131" "133" "134" "117"];
psalm_lengths = ...
[34 32 32 31 29 29 28 28 27 26 26 25 25 25 24 24 24 24 24 24 24 24 ...
24 24 23 23 23 23 23 23 23 23 22 22 22 22 22 22 21 21 21 21 21 21 ...
21 20 20 20 20 20 19 19 19 19 19 19 18 18 18 18 18 18 18 18 18 17 ...
17 17 17 17 16 16 15 15 15 15 14 14 14 14 14 14 14 14 14 14 14 ...
13 13 13 13 13 13 13 13 12 12 12 12 12 12 12 12 12 12 11 11 11 11 ...
11 11 11 11 11 10 10 10 10 10 10 10 10 9 9 9 9 9 9 9 9 9 9 9 9 8 8 ...
8 8 8 8 8 8 8 8 7 7 7 7 7 7 6 6 6 6 6 6 6 5 5 5 5 5 5 4 3 3 3 2];
% These parameters can be tweaked to produce different results.
w_val = 1.0; % weight of value balancing objective
w_seq = 1.0; % weight of sequentiality objective
target_length = mean(psalm_lengths) * 3; % target length of a triple of Psalms
max_length = target_length + 15; % maximum length of a triple of Psalms
min_length = target_length - 15; % minimum length of a triple of Psalms
% Force Psalms split into pairs to go together, codified as a key-value
% pair. Allows us to ensure that if we see <key> in a triple, that the
% triple also contains <val>.
pairs = dictionary(...
["89a" "89b" "18a" "18b" "106a" "106b" "105a" "105b" "107a" "107b" ...
"37a" "37b" "69a" "69b" "68a" "68b" "104a" "104b" "109a" "109b"], ...
["89b" "89a" "18b" "18a" "106b" "106a" "105b" "105a" "107b" "107a" ...
"37b" "37a" "69b" "69a" "68b" "68a" "104b" "104a" "109b" "109a"]);
% Ensure that these triples of Psalms are grouped together.
forced_triples = [
"120" "121" "122"
"123" "124" "125"
"126" "127" "128"
"110" "111" "112"
"78a" "78b" "78c"
];
%% Preprocessing
% The first step is to perform some simple transformations on the data so
% that it can more easily be processed.
% Exclude all "forced" Psalms from lists
all_indices = 1:N;
excluded_psalms = unique(forced_triples(:));
free_indices = all_indices(~ismember(psalm_names,excluded_psalms));
free_psalm_names_str = psalm_names(free_indices);
free_psalm_lengths = psalm_lengths(free_indices);
N_free = length(free_indices);
% Convert Psalm name notation from using trailing letters to a purely
% numeric notation (119a => 119.1). This allows us to calculate costs
% numerically.
free_psalm_names_num = zeros(N_free,1);
for i = 1:N_free
free_psalm_names_num(i) = psalmNameStrToNum(free_psalm_names_str(i));
end
%% Candidate Generation
% Now that the data is in a workable state, we generate candidate triples.
combinations = nchoosek(1:N_free,3);
N_combinations = size(combinations,1);
% Next we filter the combinations based some of our constraints.
valid_indices = true(N_combinations,1);
for k = 1:N_combinations
names = free_psalm_names_str(combinations(k,:));
lengths = free_psalm_lengths(combinations(k,:));
% Ensure that the triple is not too long or too short
total_length = sum(lengths);
if total_length > max_length || total_length < min_length
valid_indices(k) = false;
continue;
end
% Forbid multiple divisions of Psalm 119 from being in the same triple
prefix_count = sum(startsWith(names, "119"));
if prefix_count > 1
valid_indices(k) = false;
continue;
end
% Ensure that pairs are together
for i = 1:numel(names)
id = names(i);
if isKey(pairs, id) && all(~ismember(names,pairs(id)))
valid_indices(k) = false;
break;
end
end
end
combinations = combinations(valid_indices,:);
N_combinations = size(combinations,1);
%% Cost Calculation
% Next, we need to calculate a "cost" for each triple. The lower the cost,
% the "better" the triple. This cost is a function of:
% a) how far away the length is from our desired length (the average)
% b) how close the Psalms are sequentially
cost = zeros(N_combinations,1);
for k = 1:N_combinations
total_length = sum(free_psalm_lengths(combinations(k,:)));
imbalance = abs(total_length - target_length);
names = free_psalm_names_num(combinations(k,:));
spread = max(names) - min(names);
cost(k) = (w_val * imbalance) + (w_seq * spread);
end
%% Configure & Run Solver
% Now that we have built and filtered our list of qualified candidates, we
% can configure our solver variables based on our constraints and run the
% solver.
% Ensure that each Psalm is used exactly once
A = sparse(N_free, N_combinations);
for k = 1:N_combinations
A(combinations(k,:), k) = 1;
end
Aeq = [A; ones(1,N_combinations)];
% Ensure that N/3 triples are selected
beq = [ones(N_free,1); N_free/3];
intcon = 1:N_combinations;
lb = zeros(N_combinations,1);
ub = ones(N_combinations,1);
result = intlinprog(cost, intcon, [], [], Aeq, beq, lb, ub);
%% Extract & Display Results
selected = find(result > 0.5);
solution = combinations(selected,:);
psalm_triples = free_indices(solution);
final_psalms = [];
final_lengths = [];
for i = 1:size(psalm_triples,1)
t = psalm_triples(i,:);
total_length = sum(psalm_lengths(t));
final_psalms = [final_psalms; sort(psalm_names(t),'ascend')];
final_lengths = [final_lengths; total_length];
end
for i = 1:size(forced_triples,1)
t = forced_triples(i,:);
total_length = ...
psalm_lengths(psalm_names==t(1)) + ...
psalm_lengths(psalm_names==t(2)) + ...
psalm_lengths(psalm_names==t(3));
final_psalms = [final_psalms; sort(t,'ascend')];
final_lengths = [final_lengths; total_length];
end
T = table(final_psalms, final_lengths, VariableNames=["Psalms","Length"]);
T = sortrows(T,'Length','descend');
%% psalmNameStrToNum
% Helper function for converting the string name of a Psalm to a numeric
% one. E.g.: "119a" => 119.1
function n = psalmNameStrToNum(s)
token = regexp(s,'(\d+)([a-z]?)','tokens');
t = token{1};
base = str2double(t{1});
if isempty(t{2})
offset = 0;
else
offset = double(t{2}) - double('a') + 1;
end
n = base + (offset * 0.1);
end
The original number of candidate triples is close to 600,000. Filtering reduces this to a little over 300,000 triples, which helps make the solver run much faster.
After running for about 10 minutes, the program produces the following optimal solution (optimal based on the constraints and weighting of goals).
| Psalms | Length | Psalms | Length | Psalms | Length | Psalms | Length |
|---|---|---|---|---|---|---|---|
| 78a,78b,78c | 72 | 104a,104b,108 | 49 | 119f,132,133 | 45 | 136,137,138 | 43 |
| 87,89a,89b | 60 | 119a,129,130 | 48 | 53,55,56 | 45 | 50,52,54 | 43 |
| 70,73,74 | 57 | 119g,134,135 | 48 | 61,63,71 | 45 | 139,141,142 | 42 |
| 118,119e,131 | 56 | 102,98,99 | 47 | 90,93,94 | 45 | 6,8,9 | 42 |
| 15,18a,18b | 56 | 24,27,34 | 47 | 103,95,97 | 45 | 16,17,19 | 41 |
| 101,106a,106b | 56 | 38,42,46 | 47 | 51,57,58 | 45 | 140,143,144 | 41 |
| 68a,68b,72 | 56 | 23,26,35 | 46 | 75,76,77 | 45 | 3,4,7 | 36 |
| 109a,109b,119c | 55 | 20,21,25 | 46 | 145,146,148 | 45 | 147,149,150 | 35 |
| 36,37a,37b | 53 | 30,32,33 | 46 | 64,65,66 | 45 | 10,11,12 | 34 |
| 107a,107b,113 | 52 | 47,48,49 | 46 | 80,81,82 | 45 | 1,2,5 | 31 |
| 43,44,45 | 50 | 39,40,41 | 46 | 67,69a,69b | 45 | 110,111,112 | 27 |
| 114,115,119d | 50 | 13,14,22 | 45 | 79,83,84 | 45 | 120,121,122 | 24 |
| 100,105a,105b | 50 | 28,29,31 | 45 | 59,60,62 | 45 | 123,124,125 | 17 |
| 85,86,88 | 50 | 116,117,119b | 45 | 91,92,96 | 45 | 126,127,128 | 17 |
Revisions and Final Result
These results are not bad, but the next step is to see if we can revise our constraints and goals to see if we can produce an even better result.
First, I removed all of the forced triples (except 78a/78b/78c). While I liked the ones I originally specified, they were all much shorter than the average triple length. Better to see if we can provide fewer constraints and just let the solver do its job.
The second improvement was to the cost function. Originally, the function applied a cost equal to the difference between the highest and lowest Psalm number times the weight w_seq. However, this does not adequetely capture our priorities. While we highly prioritize Psalms that are sequential, if there are non-sequential Psalms, we don’t really care how far apart they are. So I tried two revisions: in the first, we apply a penalty of 100 for each nonsequential pair of Psalms in a triplet (for a max possible penalty of 200), and then 0.1 cost for each step over 1 between each pair; in the second, we apply just the 100 penalty for nonsequential Psalms, and no 0.1 penalty per step. We compare the results between these approaches below.
The final improvement is to tighten the range of triple lengths. First to average+/-10, and then to average+/-7.5 and average+/-5. The latter two produced no results, so those ranges are too tight. But +/-10 did produce results, and is a nice improvement over +/-15.
Now that we have three different Psalm groupings, we can compare them. To do this, we can categorize the groupings into the following categories (each triple gets one category, with the following order of precedence).
For ordered triple [a b c]:
- Perfectly Sequential:
(b - a) <= 1 && (c - b) <= 1 - Partial Division:
round(a) == round(b) || round(b) == round(c) - Partially Sequential:
(b - a) <= 1 || (c - b) <= 1 - Assortment: everything else
We also show the standard deviation of all the lengths.
| Forced Triples | Triple Length Range | Non-Sequential Cost | Perfectly Sequential | Partial Division | Partially Sequential | Assortment | Standard Deviation |
|---|---|---|---|---|---|---|---|
| 120,121,122 123,124,125 126,127,128 110,111,112 78a,78b,78c | +/-15 | 1.0*(c-a) | 14 | 9 | 27 | 6 | 2.271721 |
| 78a,78b,78c | +/-10 | 0.1*(c-a) 100 non-seq penalty | 31 | 9 | 16 | 0 | 4.810702 |
| 78a,78b,78c | +/-10 | 100 non-seq penalty | 31 | 8 | 17 | 0 | 4.810702 |
Of the four categories of triple, #4 “Assortment” is the least desirable. So we can immediately notice that our two revisions are vast improvements over the first attempt.
Next, between #2 “Partial Division” and #3 “Partially Sequential”, I do not think I can say that one is better than the other. Our two latter attempts only differ by one.
Also notable is that our second two attempts saw a big increase in category #1 “Perfectly Sequential”, from 14 to 31, over a 2x increase. This is by far our biggest improvement. The cost is the increase in Standard Deviation. Which I think is a cost worth paying.
Since our two revised attempts show almost identical results, including equivalent standard deviations for their lengths, it’s basically a wash, and it comes down to preference. Since, upon further reflection, I do not think the additional penalty of 0.1*(c-a) really provides much theoretical benefit (I do not necessarily see value in preferring slightly closer non-sequential Psalms, all else being equal), I have selected the last attempt as our final grouping. Here it is:
| Psalms | Length | Psalms | Length | Psalms | Length | Psalms | Length |
|---|---|---|---|---|---|---|---|
| 78a,78b,78c | 72 | 103,134,135 | 46 | 80,81,82 | 45 | 100,101,102 | 42 |
| 117,89a,89b | 55 | 10,11,9 | 46 | 113,68a,68b | 45 | 119d,98,99 | 42 |
| 50,51,52 | 55 | 47,48,49 | 46 | 114,115,116 | 45 | 65,66,67 | 42 |
| 133,18a,18b | 54 | 144,145,146 | 46 | 42,43,44 | 44 | 59,60,61 | 41 |
| 1,106a,106b | 54 | 16,17,72 | 46 | 119b,28,29 | 44 | 14,15,73 | 40 |
| 128,33,34 | 51 | 83,84,85 | 46 | 111,112,119f | 44 | 53,54,55 | 40 |
| 30,31,32 | 49 | 39,40,41 | 46 | 104a,104b,122 | 44 | 119c,12,13 | 39 |
| 105a,105b,123 | 49 | 79,90,91 | 46 | 92,93,94 | 44 | 119g,120,121 | 39 |
| 22,23,24 | 48 | 119a,124,125 | 45 | 108,45,46 | 44 | 6,7,8 | 39 |
| 139,140,141 | 48 | 118,129,130 | 45 | 136,137,138 | 43 | 19,20,21 | 39 |
| 107a,107b,127 | 48 | 119e,131,132 | 45 | 142,143,38 | 43 | 109a,109b,110 | 38 |
| 25,26,27 | 48 | 2,3,71 | 45 | 147,148,149 | 43 | 56,57,58 | 38 |
| 126,35,36 | 47 | 4,5,74 | 45 | 69a,69b,70 | 43 | 62,63,64 | 36 |
| 150,37a,37b | 46 | 75,76,77 | 45 | 86,87,88 | 43 | 95,96,97 | 36 |
However, by “final”, I do not mean we are locked into this grouping. It is likely that we will want to make some small adjustments during the process of assigning groups to hours. But more on that later.
And for completeness, here is our final program:
N = 168;
psalm_names = ...
["89b", "119a", "22", "78a", "102", "118", "35", "73", "44", "18b", "136", ...
"18a", "106b", "31", "119b", "119c", "119d", "119e", "119f", "119g", "78b", ...
"55", "71", "139", "106a", "105b", "104a", "34", "38", "50", "74", "94", "105a", ...
"107a", "37a", "25", "33", "103", "107b", "9", "49", "51", "77", "135", "145", ...
"109a", "66", "72", "80", "147", "89a", "69b", "68a", "83", "88", "116", "37b", ...
"69a", "7", "10", "40", "45", "59", "115", "132", "78c", "68b", "81", "86", "90", ...
"91", "92", "17", "19", "48", "144", "21", "27", "39", "41", "56", ...
"60", "65", "85", "108", "140", "148", "5", "30", "36", "62", "76", "79", "84", ...
"96", "104b", "2", "26", "42", "46", "57", "58", "63", "97", "143", "109b", "6", ...
"16", "29", "32", "52", "64", "75", "95", "8", "20", "24", "47", "111", "112", ...
"141", "146", "3", "4", "12", "28", "54", "61", "98", "99", "113", "122", "137", ...
"149", "67", "82", "101", "114", "121", "124", "129", "130", "138", "142", "11", ...
"14", "53", "87", "110", "120", "1", "13", "23", "70", "126", "128", "150", ...
"15", "43", "93", "100", "125", "127", "123", "131", "133", "134", "117"];
psalm_lengths = ...
[34, 32, 32, 31, 29, 29, 28, 28, 27, 26, 26, 25, 25, 25, 24, 24, 24, 24, 24, 24, 24, 24, ...
24, 24, 23, 23, 23, 23, 23, 23, 23, 23, 22, 22, 22, 22, 22, 22, 21, 21, 21, 21, 21, 21, ...
21, 20, 20, 20, 20, 20, 19, 19, 19, 19, 19, 19, 18, 18, 18, 18, 18, 18, 18, 18, 18, 17, ...
17, 17, 17, 17, 16, 16, 15, 15, 15, 15, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, ...
13, 13, 13, 13, 13, 13, 13, 13, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 11, 11, 11, 11, ...
11, 11, 11, 11, 11, 10, 10, 10, 10, 10, 10, 10, 10, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 8, 8, ...
8, 8, 8, 8, 8, 8, 8, 8, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 4, 3, 3, 3, 2];
% These parameters can be tweaked to produce different results.
w_val = 1.0; % weight of objective that triple length should be close to average
non_seq_penalty = 100; % penalty for triples having non-sequential psalms
target_length = mean(psalm_lengths) * 3; % target length of a triple of Psalms
max_length = target_length + 10; % maximum length of a triple of Psalms
min_length = target_length - 10; % minimum length of a triple of Psalms
% Force Psalms split into pairs to go together, codified as a key-value
% pair. Allows us to ensure that if we see <key> in a triple, that the
% triple also contains <val>.
pairs = dictionary( ...
["89a", "89b", "18a", "18b", "106a", "106b", "105a", "105b", "107a", "107b", ...
"37a", "37b", "69a", "69b", "68a", "68b", "104a", "104b", "109a", "109b"], ...
["89b", "89a", "18b", "18a", "106b", "106a", "105b", "105a", "107b", "107a", ...
"37b", "37a", "69b", "69a", "68b", "68a", "104b", "104a", "109b", "109a"]);
% Ensure that these triples of Psalms are grouped together.
forced_triples = [
"78a", "78b", "78c"
];
%% Preprocessing
% The first step is to perform some simple transformations on the data so
% that it can more easily be processed.
% Exclude all "forced" Psalms from lists
all_indices = 1 : N;
excluded_psalms = unique(forced_triples(:));
free_indices = all_indices(~ismember(psalm_names, excluded_psalms));
free_psalm_names_str = psalm_names(free_indices);
free_psalm_lengths = psalm_lengths(free_indices);
N_free = length(free_indices);
% Convert Psalm name notation from using trailing letters to a purely
% numeric notation (119a => 119.1). This allows us to calculate costs
% numerically.
free_psalm_names_num = zeros(N_free, 1);
for i = 1 : N_free
free_psalm_names_num(i) = psalmNameStrToNum(free_psalm_names_str(i));
end
%% Candidate Generation
% Now that the data is in a workable state, we generate candidate triples.
combinations = nchoosek(1 : N_free, 3);
N_combinations = size(combinations, 1);
% Next we filter the combinations based some of our constraints.
valid_indices = true(N_combinations, 1);
for k = 1 : N_combinations
names = free_psalm_names_str(combinations(k, :));
lengths = free_psalm_lengths(combinations(k, :));
% Ensure that the triple is not too long or too short
total_length = sum(lengths);
if total_length > max_length || total_length < min_length
valid_indices(k) = false;
continue;
end
% Forbid multiple divisions of Psalm 119 from being in the same triple
prefix_count = sum(startsWith(names, "119"));
if prefix_count > 1
valid_indices(k) = false;
continue;
end
% Ensure that pairs are together
for i = 1 : numel(names)
id = names(i);
if isKey(pairs, id) && ~any(ismember(names, pairs(id)))
valid_indices(k) = false;
break;
end
end
end
combinations = combinations(valid_indices, :);
N_combinations = size(combinations, 1);
%% Cost Calculation
% Next, we need to calculate a "cost" for each triple. The lower the cost,
% the "better" the triple. This cost is a function of:
% a) how far away the length is from our desired length (the average)
% b) how close the Psalms are sequentially
cost = zeros(N_combinations, 1);
for k = 1 : N_combinations
total_length = sum(free_psalm_lengths(combinations(k, :)));
imbalance = abs(total_length - target_length);
% Review the distance between the 1st/2nd and 2nd/3rd Psalm, sequentially.
% Our priority is to give a big penalty to non-sequential Psalms.
penalty = 0;
names = sort(free_psalm_names_num(combinations(k, :)), 'ascend');
for i = 2 : 3
difference = names(i) - names(i - 1);
if difference > 1
penalty = penalty + non_seq_penalty;
end
end
cost(k) = (w_val * imbalance) + penalty;
end
%% Configure & Run Solver
% Now that we have built and filtered our list of qualified candidates, we
% can configure our solver variables based on our constraints and run the
% solver.
% Ensure that each Psalm is used exactly once
A = sparse(N_free, N_combinations);
for k = 1 : N_combinations
A(combinations(k, :), k) = 1;
end
Aeq = [A; ones(1, N_combinations)];
% Ensure that N/3 triples are selected
beq = [ones(N_free, 1); N_free / 3];
intcon = 1 : N_combinations;
lb = zeros(N_combinations, 1);
ub = ones(N_combinations, 1);
result = intlinprog(cost, intcon, [], [], Aeq, beq, lb, ub);
%% Extract & Display Results
selected = find(result > 0.5);
solution = combinations(selected, :);
psalm_triples = free_indices(solution);
final_psalms = [];
final_lengths = [];
for i = 1 : size(psalm_triples, 1)
t = psalm_triples(i, :);
total_length = sum(psalm_lengths(t));
final_psalms = [final_psalms; sort(psalm_names(t), 'ascend')];
final_lengths = [final_lengths; total_length];
end
for i = 1 : size(forced_triples, 1)
t = forced_triples(i, :);
total_length = ...
psalm_lengths(psalm_names == t(1)) + ...
psalm_lengths(psalm_names == t(2)) + ...
psalm_lengths(psalm_names == t(3));
final_psalms = [final_psalms; sort(t, 'ascend')];
final_lengths = [final_lengths; total_length];
end
T = table(final_psalms, final_lengths, VariableNames = ["Psalms", "Length"]);
T = sortrows(T, 'Length', 'descend');
%% psalmNameStrToNum
% Helper function for converting the string name of a Psalm to a numeric
% one. E.g.: "119a" => 119.1
function n = psalmNameStrToNum(s)
token = regexp(s, '(\d+)([a-z]?)', 'tokens');
t = token{1};
base = str2double(t{1});
if isempty(t{2})
offset = 0;
else
offset = double(t{2}) - double('a') + 1;
end
n = base + (offset * 0.1);
end
Filling the Calendar
Comparing Psalm Placement in Existing Psalters
Before determining how we ought to place our 56 triples, we should look to see how our previously-reviewed Psalters place each Psalm. In addition, I have personally reviewed each Psalm to see if I have a particular inclination for how it should be placed. This is entirely personal preference. For example, for me, Psalm 51 (very penitential) is a Friday Psalm, as is Psalm 22 (prefiguring the Passion of our Lord). I have captured all this information in the following table:
| Psalm | Monastic | Pius X | LotH | Personal Inclinations | ||||
|---|---|---|---|---|---|---|---|---|
| Day(s) | Hour(s) | Day(s) | Hour(s) | Day(s) | Hour(s) | Day(s) | Hour(s) | |
| 1 | Mon | Prime | Sun | Matins | Sun | Readings | - | - |
| 2 | Mon | Prime | Sun | Matins | Sun | Readings | - | - |
| 3 | Sun Mon Tue Wed Thu Fri Sat | Matins | Sun | Matins | Sun | Readings | - | - |
| 4 | Sun Mon Tue Wed Thu Fri Sat | Compline | Sun | Compline | Sat | Night | - | - |
| 5 | Mon | Lauds | Mon | Lauds | Mon | Morning | - | Morning |
| 6 | Mon | Prime | Mon | Compline | Mon | Readings | - | Evening |
| 7 | Tue | Prime | Mon | Compline | Mon | Midday | - | - |
| 8 | Tue | Prime | Sun | Matins | Sat | Morning | - | - |
| 9 | Tue | Prime | Sun | Matins | Mon | Readings | - | - |
| 10 | Wed | Prime | Sun | Matins | Tue | Readings | - | - |
| 11 | Wed | Prime | Sun | Matins | Mon | Evening | - | - |
| 12 | Wed | Prime | Tue | Compline | Tue | Readings | - | - |
| 13 | Thu | Prime | Tue | Compline | Tue | Midday | - | - |
| 14 | Thu | Prime | Mon | Matins | Tue | Midday | - | - |
| 15 | Thu | Prime | Mon | Matins | Mon | Evening | - | - |
| 16 | Fri | Prime | Tue | Compline | Sat Thu | Evening Night | - | Evening |
| 17 | Fri | Prime | Mon | Matins | Wed | Midday | - | - |
| 18 | Fri Sat | Prime | Mon | Matins | Thu Wed | Readings | - | - |
| 19 | Sat | Prime | Mon | Prime | Mon | Midday Morning | Sun | - |
| 20 | Sat | Prime | Mon | Matins | Tue | Evening | - | - |
| 21 | Sun | Matins | Mon | Matins | Tue | Evening | - | - |
| 22 | Sun | Matins | Fri | Prime | Fri | Midday | Fri | - |
| 23 | Sun | Matins | Thu | Prime | Sun | Midday | - | - |
| 24 | Sun | Matins | Mon | Prime | Sun Tue | Morning Readings | - | - |
| 25 | Sun | Matins | Tue | Prime | Thu | Midday | - | - |
| 26 | Sun | Matins | Wed | Prime | Fri | Midday | - | - |
| 27 | Sun | Matins | Mon | Terce | Wed | Evening | - | - |
| 28 | Sun | Matins | Mon | Terce | Fri | Midday | - | - |
| 29 | Sun | Matins | Mon | Lauds | Mon | Morning | - | - |
| 30 | Sun | Matins | Mon | Matins | Thu | Evening | - | - |
| 31 | Sun | Matins | Mon | Sext | Mon Wed | Night Readings | - | - |
| 32 | Sun | Matins | Mon | None | Thu | Evening | - | - |
| 33 | Mon | Matins | Mon | None | Tue | Morning | - | - |
| 34 | Mon | Matins | Wed | Compline | Sat | Midday | - | - |
| 35 | Mon | Matins | Tue | Matins | Fri | Readings | - | - |
| 36 | Mon | Lauds | Thu | Lauds | Wed | Morning | - | - |
| 37 | Mon | Matins | Tue | Matins | Tue | Readings | - | - |
| 38 | Mon | Matins | Tue | Matins | Fri | Readings | - | - |
| 39 | Mon | Matins | Tue | Matins | Wed | Readings | - | - |
| 40 | Mon | Matins | Tue | Terce | Mon | Midday | - | - |
| 41 | Mon | Matins | Tue | Sext | Fri | Evening | - | - |
| 42 | Mon | Matins | Tue | Sext | Mon | Morning | - | - |
| 43 | Tue | Lauds | Tue | Lauds | Tue | Morning | Sun | Morning |
| 44 | Mon | Matins | Tue | None | Thu | Readings | - | - |
| 45 | Mon | Matins | Wed | Matins | Mon Sat | Evening Midday | - | - |
| 46 | Tue | Matins | Wed | Matins | Fri | Evening | - | - |
| 47 | Tue | Matins | Mon | Lauds | Wed | Morning | - | - |
| 48 | Tue | Matins | Wed | Matins | Thu | Morning | - | - |
| 49 | Tue | Matins | Wed | Matins | Tue | Evening | - | - |
| 50 | Tue | Matins | Wed | Matins | Mon Sat | Readings | - | - |
| 51 | Sun Mon Tue Wed Thu Fri Sat | Lauds | Wed | Matins | Fri | Morning | Fri | - |
| 52 | Tue | Matins | Wed | Prime | Wed | Readings | - | - |
| 53 | Tue | Matins | Wed | Prime | Tue | Midday | - | - |
| 54 | Tue | Matins | Wed | Terce | Tue | Midday | - | - |
| 55 | Tue | Matins | Wed | Terce | Fri Wed | Midday Readings | - | - |
| 56 | Tue | Matins | Wed | Sext | Thu | Midday | - | - |
| 57 | Tue | Lauds | Wed | Sext | Thu | Midday Morning | - | - |
| 58 | Tue | Matins | Wed | Sext | - | - | - | - |
| 59 | Tue | Matins | Wed | None | Fri | Midday | - | - |
| 60 | Wed | Matins | Wed | None | Fri | Midday | - | - |
| 61 | Wed | Matins | Wed | Compline | Sat | Midday | - | - |
| 62 | Wed | Matins | Thu | Matins | Wed | Evening | - | - |
| 63 | Sun | Lauds | Sun | Lauds | Sun | Morning | Sun | Morning |
| 64 | Wed | Lauds | Sat | Lauds | Sat | Midday | - | - |
| 65 | Wed | Lauds | Wed | Lauds | Tue | Morning | - | - |
| 66 | Wed | Matins | Thu | Matins | Sun | Readings | - | - |
| 67 | Sun Mon Tue Wed Thu Fri Sat | Lauds | Tue | Lauds | Tue Wed | Evening Morning | - | - |
| 68 | Wed | Matins | Thu | Matins | Tue | Readings | - | - |
| 69 | Wed | Matins | Thu | Matins | Fri | Readings | - | - |
| 70 | Wed | Matins | Thu | Compline | Wed | Midday | - | - |
| 71 | Wed | Matins | Thu | Compline | Mon | Midday | - | - |
| 72 | Wed | Matins | Thu | Prime | Thu | Evening | - | - |
| 73 | Wed | Matins | Thu | Terce | Mon | Readings | - | - |
| 74 | Thu | Matins | Thu | Sext | Tue | Midday | - | - |
| 75 | Thu | Matins | Thu | None | Wed | Midday | - | - |
| 76 | Fri | Lauds | Thu | None | Sun | Midday | - | - |
| 77 | Thu | Matins | Fri | Compline | Wed | Morning | - | - |
| 78 | Thu | Matins | Fri | Matins | - | - | - | - |
| 79 | Thu | Matins | Fri | Matins | Thu | Midday | - | - |
| 80 | Thu | Matins | Fri | Terce | Thu | Midday Morning | - | - |
| 81 | Thu | Matins | Fri | Matins | Thu | Morning | - | - |
| 82 | Thu | Matins | Fri | Terce | Mon | Midday | - | - |
| 83 | Thu | Matins | Fri | Matins | - | - | - | - |
| 84 | Thu | Matins | Fri | Sext | Mon | Morning | Sun | Morning |
| 85 | Thu | Matins | Fri | Lauds | Tue | Morning | - | - |
| 86 | Fri | Matins | Fri | Compline | Mon Wed | Morning Night | - | - |
| 87 | Fri | Matins | Fri | Sext | Thu | Morning | - | - |
| 88 | Thu | Lauds | Sat | Compline | Fri Tue | Midday Night | Fri | Evening |
| 89 | Fri | Matins | Fri | None | Thu Wed | Readings | - | - |
| 90 | Thu | Lauds | Thu | Lauds | Mon Thu | Morning Readings | - | - |
| 91 | Sun Mon Tue Wed Thu Fri Sat | Compline | Sun | Compline | Sun | Night | Sun | Evening |
| 92 | Fri | Lauds | Sat | Lauds | Sat | Morning | - | - |
| 93 | Fri | Matins | Sun | Lauds | Sun | Morning | - | - |
| 94 | - | - | Sat | Prime | Wed | Midday | - | - |
| 95 | Sun Mon Tue Wed Thu Fri Sat | Matins | - | - | - | - | - | Morning |
| 96 | Fri | Matins | Tue | Lauds | Mon | Morning | - | - |
| 97 | Fri | Matins | Wed | Lauds | Wed | Morning | - | - |
| 98 | Fri | Matins | Thu | Lauds | Wed | Morning | - | - |
| 99 | Fri | Matins | Fri | Lauds | Thu | Morning | - | - |
| 100 | Fri | Matins | Sun | Lauds | Fri | Morning | - | - |
| 101 | Fri | Matins | Wed | Lauds | Tue | Morning | - | Morning |
| 102 | Sat | Matins | Sat | Terce | Tue | Readings | - | - |
| 103 | Sat | Matins | Sat | Compline | Wed | Readings | - | - |
| 104 | Sat | Matins | Sat | Sext | Sun | Readings | - | - |
| 105 | Sat | Matins | Sat | Matins | - | - | - | - |
| 106 | Sat | Matins | Sat | Matins | - | - | - | - |
| 107 | Sat | Matins | Sat | Matins | Sat | Readings | - | - |
| 108 | Sat | Matins | Sat | Prime | Wed | Morning | - | Morning |
| 109 | Sat | Matins | Sat | None | - | - | - | - |
| 110 | Sun | Vespers | Sun | Vespers | Sun | Evening | Sun | Evening |
| 111 | Sun | Vespers | Sun | Vespers | Sun | Evening | - | - |
| 112 | Sun | Vespers | Sun | Vespers | Sun | Evening | - | - |
| 113 | Mon Sun | Vespers | Sun | Vespers | Sat | Evening | - | - |
| 114 | - | - | - | - | Sun | Evening | - | - |
| 115 | - | - | - | - | Sun | Evening | - | - |
| 116 | Mon | Vespers | Mon | Vespers | Fri Sat | Evening | Sun | Morning |
| 117 | Mon | Vespers | Mon | Lauds | Sat | Morning | - | - |
| 118 | Sun | Lauds | Sun | Prime | Sun | Midday Morning | Sun | - |
| 119 | Mon Sun | None Prime Sext Terce | Sun | None Prime Sext Terce | Mon Tue Wed Thu Fri Sat | Evening Midday Morning | - | Morning |
| 120 | Tue Wed Thu Fri Sat | Terce | Mon | Vespers | Mon | Midday | - | - |
| 121 | Tue Wed Thu Fri Sat | Terce | Mon | Vespers | Fri | Evening | - | - |
| 122 | Tue Wed Thu Fri Sat | Terce | Mon | Vespers | Sat | Evening | - | - |
| 123 | Tue Wed Thu Fri Sat | Sext | Tue | Vespers | Mon | Evening | - | - |
| 124 | Tue Wed Thu Fri Sat | Sext | Tue | Vespers | Mon | Evening | - | - |
| 125 | Tue Wed Thu Fri Sat | Sext | Tue | Vespers | Tue | Evening | - | - |
| 126 | Tue Wed Thu Fri Sat | None | Tue | Vespers | Wed | Evening | - | - |
| 127 | Tue Wed Thu Fri Sat | None | Tue | Vespers | Wed | Evening | - | - |
| 128 | Tue Wed Thu Fri Sat | None | Wed | Vespers | Thu | Midday | - | - |
| 129 | Mon | Vespers | Wed | Vespers | Thu | Midday | - | - |
| 130 | Tue | Vespers | Wed | Vespers | Sat Wed | Evening Night | - | - |
| 131 | Tue | Vespers | Wed | Vespers | Sat Tue | Evening Readings | - | - |
| 132 | Tue | Vespers | Wed | Vespers | Sat Thu | Evening Readings | - | - |
| 133 | Tue | Vespers | Thu | Vespers | Fri | Midday | - | - |
| 134 | Sun Mon Tue Wed Thu Fri Sat | Compline | Sun | Compline | Sat | Night | - | - |
| 135 | Wed | Vespers | Tue | Lauds | Fri Mon | Evening Morning | Sun | - |
| 136 | Wed | Vespers | Thu | Vespers | Mon Sat | Evening Readings | Sun | - |
| 137 | Wed | Vespers | Thu | Vespers | Tue | Evening | - | - |
| 138 | Wed | Vespers | Thu | Vespers | Tue | Evening | - | - |
| 139 | Thu | Vespers | Fri | Vespers | Wed | Evening | - | - |
| 140 | Thu | Vespers | Fri | Vespers | Fri | Midday | - | - |
| 141 | Thu | Vespers | Fri | Vespers | Sat | Evening | - | - |
| 142 | Fri Sat | Lauds Vespers | Fri | Vespers | Sat | Evening | - | - |
| 143 | Sat | Lauds | Fri | Lauds | Thu Tue | Morning Night | - | - |
| 144 | Fri | Vespers | Sat | Vespers | Thu Tue | Evening Morning | - | - |
| 145 | Fri Sat | Vespers | Sat | Vespers | Fri Sun | Evening Readings | - | - |
| 146 | Sat | Vespers | Wed | Lauds | Wed | Morning | - | - |
| 147 | Sat | Vespers | Fri Thu | Lauds | Fri Thu | Morning | - | - |
| 148 | Sun Mon Tue Wed Thu Fri Sat | Lauds | Sun | Lauds | Sun | Morning | - | - |
| 149 | Sun Mon Tue Wed Thu Fri Sat | Lauds | Sat | Lauds | Sun | Morning | - | - |
| 150 | Sun Mon Tue Wed Thu Fri Sat | Lauds | Sat | Lauds | Sun | Morning | - | - |
Analyzing the Results
Next Steps and Future Work
Notes and References
- Schemas retrieved from http://www.gregorianbooks.com/gregorian/www/www.kellerbook.com/SCHEMA~1.HTM. These schemas here use the Greek numbering, which why it was preserved as the default on this page. The schemas also had incomplete data about which instances of Psalms were repetitions and which were divisions. An effort was made to add this data to the LotH table, but not to the others.