diff --git a/cmd/common.go b/cmd/common.go
new file mode 100644
index 0000000..2ab9efa
--- /dev/null
+++ b/cmd/common.go
@@ -0,0 +1,32 @@
+/*
+Copyright © 2026 filifa
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+package cmd
+
+import (
+ "bufio"
+ "os"
+)
+
+func splitLines(file *os.File) []string {
+ slice := make([]string, 0)
+ scanner := bufio.NewScanner(file)
+ for scanner.Scan() {
+ slice = append(slice, scanner.Text())
+ }
+
+ return slice
+}
diff --git a/cmd/convolve.go b/cmd/convolve.go
new file mode 100644
index 0000000..7ce2573
--- /dev/null
+++ b/cmd/convolve.go
@@ -0,0 +1,110 @@
+/*
+Copyright © 2026 filifa
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+package cmd
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "strconv"
+
+ "github.com/spf13/cobra"
+ "scm.dairydemon.net/filifa/mathtools/internal/lib"
+)
+
+var convolveA string
+var convolveB string
+
+func readSequenceFromFile(filepath string) ([]complex128, error) {
+ // initial length 1 so index math works
+ seq := make([]complex128, 1)
+ file, err := os.Open(filepath)
+ if err != nil {
+ return seq, err
+ }
+ defer file.Close()
+
+ lines := splitLines(file)
+ for _, line := range lines {
+ x, err := strconv.ParseComplex(line, 128)
+ if err != nil {
+ return seq, err
+ }
+
+ seq = append(seq, x)
+ }
+
+ return seq, nil
+}
+
+func convolve(cmd *cobra.Command, args []string) {
+ a, err := readSequenceFromFile(convolveA)
+ if err != nil {
+ cobra.CheckErr(err)
+ }
+
+ b, err := readSequenceFromFile(convolveB)
+ if err != nil {
+ cobra.CheckErr(err)
+ }
+
+ bufStdout := bufio.NewWriter(os.Stdout)
+ defer bufStdout.Flush()
+
+ for i, n := range lib.DirichletConvolve(a, b) {
+ if i == 0 {
+ continue
+ }
+
+ if imag(n) == 0 {
+ fmt.Fprintln(bufStdout, real(n))
+ } else {
+ fmt.Fprintln(bufStdout, n)
+ }
+ }
+}
+
+// convolveCmd represents the convolve command
+var convolveCmd = &cobra.Command{
+ Use: "convolve",
+ Short: "Compute the Dirichlet convolution of two sequences",
+ Long: `Compute the Dirichlet convolution of two sequences.
+
+Each sequence is provided as a file, where line k of the file gives the kth term of the sequence.
+`,
+ Run: convolve,
+}
+
+func init() {
+ rootCmd.AddCommand(convolveCmd)
+
+ // Here you will define your flags and configuration settings.
+
+ // Cobra supports Persistent Flags which will work for this command
+ // and all subcommands, e.g.:
+ // convolveCmd.PersistentFlags().String("foo", "", "A help for foo")
+
+ // Cobra supports local flags which will only run when this command
+ // is called directly, e.g.:
+ // convolveCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+
+ convolveCmd.Flags().StringVarP(&convolveA, "first", "a", "", "first sequence")
+ convolveCmd.MarkFlagRequired("first")
+
+ convolveCmd.Flags().StringVarP(&convolveB, "second", "b", "", "second sequence")
+ convolveCmd.MarkFlagRequired("second")
+}
diff --git a/cmd/shoelace.go b/cmd/shoelace.go
index 6652267..c76ec38 100644
--- a/cmd/shoelace.go
+++ b/cmd/shoelace.go
@@ -17,7 +17,6 @@ along with this program. If not, see .
package cmd
import (
- "bufio"
"fmt"
"os"
"strconv"
@@ -29,16 +28,6 @@ import (
var shoelaceFile string
-func splitLines(file *os.File) []string {
- slice := make([]string, 0)
- scanner := bufio.NewScanner(file)
- for scanner.Scan() {
- slice = append(slice, scanner.Text())
- }
-
- return slice
-}
-
func readFromFile(filepath string) ([]lib.Point, error) {
points := make([]lib.Point, 0)
file, err := os.Open(filepath)
diff --git a/internal/lib/convolve.go b/internal/lib/convolve.go
new file mode 100644
index 0000000..2931ff1
--- /dev/null
+++ b/internal/lib/convolve.go
@@ -0,0 +1,32 @@
+/*
+Copyright © 2026 filifa
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+package lib
+
+func DirichletConvolve(a, b []complex128) []complex128 {
+ c := make([]complex128, min(len(a), len(b)))
+ for i, x := range a {
+ for j, y := range b {
+ if i*j >= len(c) {
+ break
+ }
+
+ c[i*j] += x * y
+ }
+ }
+
+ return c
+}