Showing posts with label code. Show all posts
Showing posts with label code. Show all posts

Thursday, 5 February 2026

Why Is 313131 An Interesting Number?


Recently a friend of mine who was staying at a hotel revealed that the six digit security code for the room was 313131. This looked like an easy code to crack and I was reminded of a post that I'd made recently titled Passcodes and Repeated Digits in November of 2025. Figure 1 shows the probabilities for the number of distinct digits chosen:


Figure 1

So the code 313131 is indeed easy to crack, requiring only a maximum of 62 attempts or 31 attempts on average. However, what was of most interest to me regarding 313131 was its prime factorisation:$$ 313131=3 \times 7 \times 13 \times 31 \times 37$$If we rearrange the order of multiplication we get the following:$$ 313131=7 \times 3 \times 13 \times 31 \times 37$$Concatenating these digits we get the number \(73133137\) which is palindromic.

Naturally, I wondered how many other numbers share this property and so I set Gemini to investigate this with the following prompt:

Write a program in Python, tailored for insertion into SageMathCell or a Jupyter notebook, that identifies all composite numbers from 4 up to a user chosen upper limit with the property that the prime factors, when arranged in a suitable order and then concatenated, produce a palindromic number. The default upper limit can be set to 40000. An example of such a number would be 313131 = 3 x 7 x 13 x 31 x 37 that can written as 7 x 3 x 13 x 31 x 37 to produce the palindromic number 73133137 when concatenated. The output of the program should be a table showing number and factorisation followed by a comma separated list of the qualifying numbers.

Naturally this a processor intensive process and my Jupyter notebook struggled mightily to generate the list of suitable numbers. After some time, I decided to restrict the range to between 28000 and 29000 and after further modifications by Gemini, I was able to achieve the following list of numbers:

28072, 28125, 28194, 28224, 28242, 28273, 28308, 28322, 28332, 28416, 28431, 28448, 28585, 28589, 28593, 28601, 28602, 28609, 28620, 28672, 28685, 28692, 28750, 28800, 28812, 28847, 28951

Figure 2 shows the details:


Figure 2

I've incorporated this algorithm into my daily search program that has become somewhat impressive if I do say so myself. See Figure 3 for the results of 28067, my diurnal age today.


Figure 3

The output is generated by the same program but I've just restricted the range to my daily number, rather lazy but it does the job. Remember the prime factorisation takes multiplicity into account. For completeness I'll include the Python code:

import sys

def get_prime_factors(n):
    """Returns a list of prime factors of n."""
    factors = []
    d = 2
    temp = n
    while d * d <= temp:
        while temp % d == 0:
            factors.append(d)
            temp //= d
        d += 1
    if temp > 1:
        factors.append(temp)
    return factors

def is_palindrome(s):
    """Checks if a string is a palindrome."""
    return s == s[::-1]

def distinct_permutations(iterable):
    """
    Yields unique permutations of items in iterable.
    This handles repeated elements efficiently (e.g., [2, 2, 3])
    without generating all n! redundant combinations.
    """
    # Sort the list to start with the lexicographically first permutation
    s = sorted(iterable)
    yield tuple(s)
    
    n = len(s)
    while True:
        # 1. Find the largest index i such that s[i] < s[i+1]
        i = n - 2
        while i >= 0 and s[i] >= s[i+1]:
            i -= 1
        
        if i == -1:
            return # All permutations generated
        
        # 2. Find the largest index j such that s[i] < s[j]
        j = n - 1
        while s[j] <= s[i]:
            j -= 1
        
        # 3. Swap s[i] and s[j]
        s[i], s[j] = s[j], s[i]
        
        # 4. Reverse the sequence from s[i+1] up to the end
        s[i+1:] = s[i+1:][::-1]
        
        yield tuple(s)

def find_palindromic_composites(start_n, end_n):
    results = []
    actual_start = max(4, start_n)

    for n in range(actual_start, end_n + 1):
        factors = get_prime_factors(n)
        
        # Skip primes
        if len(factors) < 2:
            continue
            
        found_property = False
        winning_perm = None
        winning_concat = None
        
        # Use the optimized generator instead of itertools.permutations
        for p in distinct_permutations(factors):
            concat_str = "".join(map(str, p))
            
            if is_palindrome(concat_str):
                found_property = True
                winning_perm = p
                winning_concat = concat_str
                break 
        
        if found_property:
            fact_str = " x ".join(map(str, winning_perm))
            results.append((n, fact_str, winning_concat))

    return results

# --- Configuration ---
LOWER_BOUND = 28000
UPPER_BOUND = 29000

# --- Main Execution ---
print(f"Searching for composite numbers between {LOWER_BOUND} and {UPPER_BOUND}...")
print("-" * 80)
print(f"{'Number':<8} | {'Valid Factor Ordering':<35} | {'Resulting Palindrome'}")
print("-" * 80)

qualifying_numbers = find_palindromic_composites(LOWER_BOUND, UPPER_BOUND)

if not qualifying_numbers:
    print(f"No qualifying numbers found in the range {LOWER_BOUND} to {UPPER_BOUND}.")
else:
    for num, factor_str, pal_str in qualifying_numbers:
        print(f"{num:<8} | {factor_str:<35} | {pal_str}")

    print("-" * 80)
    print("\nComma separated list of qualifying numbers:")
    print(", ".join(str(r[0]) for r in qualifying_numbers))

Saturday, 20 December 2025

Cryptographic Application of Building Block Numbers

Given that any number up to 243373 can be represented using only the building blocks 1, 2, 4, 11, 25, 64, 171, 569, 3406 and 27697, I thought that capital letters could be used to represent these building block numbers: A for 1, B or 2, C for 4, D for 11, E for 25, F for 64, G for 171, H for 569, I for 3406 and J for 27697. 

With the input of a given natural number, I asked Gemini to output the building block representations using these letters, retaining brackets and any + signs but omitting any multiplication signs (as in algebra). Thus 12 = (1 + 2) * 4 would become (A + B) C. I'm was thinking in terms of cryptography where the number will be disguised using the alphabet cyphers just described and only be dicipherable using the key (A=1, B=2 and C=4 etc.). The program only needs to deal with building blocks up to 27697. 


Infographic generated by Nano Bananas using this blog's content.

Gemini duly came up with the Python program and Figure 1 shows the output:

Figure 1:
see Python code below

The program times out in SageMathCell so I've included the Python code below for reference. If making practical use of this form of encryption, one of these representations would be chosen at random and used to replace the decimal number.

*******************************
PYTHON CODE
(courtesy of Gemini)

import itertools

def solve_oeis_crypto_cipher(target):
    """
    Finds algebraic representations of 'target' using OEIS A086424 terms.
    Outputs the result using Letter Ciphers (A=1, B=2, ...).
    
    Rules:
    - A=1, B=2, C=4, D=11, E=25, F=64, G=171, H=569, I=3406, J=27697
    - Multiplication (*) is implicit (omitted). e.g., A*B -> AB
    - Addition (+) is retained.
    - Brackets included only when necessary.
    """
    
    # 1. Define the Cipher Mapping
    # We only go up to J (27697) as requested
    term_map = {
        1: 'A', 2: 'B', 4: 'C', 11: 'D', 25: 'E', 
        64: 'F', 171: 'G', 569: 'H', 3406: 'I', 27697: 'J'
    }
    
    full_sequence = sorted(term_map.keys())
    
    # Filter terms to only those small enough to be useful
    terms = [x for x in full_sequence if x <= target]
    n = len(terms)
    
    # DP State: dp[mask][value] = set of Canonical Tuples
    # Tuple Structure: ('TYPE', (operands...))
    dp = [{} for _ in range(1 << n)]
    
    # Helper to generate a consistent sort key for uniqueness
    def get_sort_key(item):
        return str(item)

    # Initialize with single terms
    for i in range(n):
        val = terms[i]
        mask = (1 << i)
        dp[mask][val] = { ('NUM', val) }

    # Iterate through mask sizes
    for r in range(2, n + 1):
        for indices in itertools.combinations(range(n), r):
            mask = sum(1 << i for i in indices)
            set_bits = [b for b in range(n) if (mask >> b) & 1]
            
            # Split mask
            for k in range(1, r // 2 + 1):
                for sub_indices in itertools.combinations(set_bits, k):
                    s = sum(1 << i for i in sub_indices)
                    comp = mask - s # SageMath safe subtraction
                    
                    if k * 2 == r and s > comp: continue
                    if not dp[s] or not dp[comp]: continue
                    
                    # Combine results from submasks
                    for v1, exprs1 in dp[s].items():
                        for v2, exprs2 in dp[comp].items():
                            
                            for e1 in exprs1:
                                for e2 in exprs2:
                                    
                                    # --- ADDITION ---
                                    res_sum = v1 + v2
                                    if res_sum <= target:
                                        ops = []
                                        # Flatten left
                                        if e1[0] == 'SUM': ops.extend(e1[1])
                                        else: ops.append(e1)
                                        # Flatten right
                                        if e2[0] == 'SUM': ops.extend(e2[1])
                                        else: ops.append(e2)
                                        
                                        ops.sort(key=get_sort_key)
                                        new_struct = ('SUM', tuple(ops))
                                        
                                        if res_sum not in dp[mask]: dp[mask][res_sum] = set()
                                        dp[mask][res_sum].add(new_struct)
                                    
                                    # --- MULTIPLICATION ---
                                    # Skip *1 (Identity)
                                    if v1 == 1 or v2 == 1: continue
                                    
                                    res_prod = v1 * v2
                                    if res_prod <= target:
                                        ops = []
                                        # Flatten left
                                        if e1[0] == 'PROD': ops.extend(e1[1])
                                        else: ops.append(e1)
                                        # Flatten right
                                        if e2[0] == 'PROD': ops.extend(e2[1])
                                        else: ops.append(e2)
                                        
                                        ops.sort(key=get_sort_key)
                                        new_struct = ('PROD', tuple(ops))
                                        
                                        if res_prod not in dp[mask]: dp[mask][res_prod] = set()
                                        dp[mask][res_prod].add(new_struct)

    # --- Formatting Function for Cipher Output ---
    def format_cipher(expr):
        etype, content = expr
        
        if etype == 'NUM':
            # Return the Cipher Letter
            return term_map[content]
            
        elif etype == 'SUM':
            # Join with "+"
            return "+".join(format_cipher(c) for c in content)
            
        elif etype == 'PROD':
            parts = []
            for c in content:
                s = format_cipher(c)
                # Add brackets ONLY if the child is a SUM
                if c[0] == 'SUM':
                    s = f"({s})"
                parts.append(s)
            # Join with empty string (Implicit multiplication)
            return "".join(parts)

    # --- Collect and Print Solutions ---
    solutions = []
    # Check every mask for the target value
    for m in range(1 << n):
        if target in dp[m]:
            for struct in dp[m][target]:
                solutions.append(format_cipher(struct))
    
    if not solutions:
        return [f"No solution found for {target}."]
    
    # Sort by length for readability
    return sorted(list(set(solutions)), key=len)

# --- User Input Section ---
target_integer = 28020

# Run
results = solve_oeis_crypto_cipher(target_integer)

# Output
print(f"Cipher Results for {target_integer}:")
print(f"Key: A=1, B=2, C=4, D=11, E=25, F=64, G=171, H=569, I=3406, J=27697")
print("-" * 60)
for res in results:
    print(res)

Monday, 8 December 2025

New Project

As I've written in earlier posts, the site \( \textbf{Numbers Aplenty}\) has been unreliable for quite some time now, often going down for days. For this reason, I developed a SageMath program that produced a similar but even more extensive output. It does the job but my coding skills are somewhat primitive and I made the mistake of getting Gemini to revise my code which it struggled with. It seems that Gemini is more comfortable coding in Python rather than SageMath that is built on top of Python. Sometime the SageMath code malfuntioned. I think it's better to get Gemini to stick with Python when coding.

I asked Gemini the following question:

Numbers Aplenty is a website that I regularly make use of but of late it has become increasingly unreliable because of down time. Is it possible to create a Python program that replicates the output of Numbers Aplenty and also allows me to add additional output of my own. I’m just asking about the feasibility of such a program, you don’t have to create any code. The URL is numbersaplenty.com.

 Yes, it is absolutely feasible to create a Python program that replicates the functionality of Numbers Aplenty. In fact, this is an excellent project for Python because the language excels at the exact type of tasks the website performs: handling large integers and running logical checks.

Here is a breakdown of why this is feasible and how you would likely approach it:

1. Python is Built for This

  • Arbitrary Precision Integers: Unlike many other languages, Python handles massive integers automatically. If you want to check a number with 50 digits (far beyond Numbers Aplenty's 15-digit limit), Python can handle it natively without needing special code.

  • No Downtime: Since the code runs locally on your machine, you never have to worry about the website being offline.

2. Libraries Do the Heavy Lifting

You would not need to write the complex math from scratch. The Python ecosystem has powerful libraries that already "know" number theory:

  • SymPy: This is the most critical library for your goal. It is a symbolic mathematics library that includes built-in functions for:

    • is_prime(n) (Checks if a number is prime)

    • factorint(n) (Finds prime factorization)

    • divisors(n) (Lists all divisors)

    • is_square(n), is_perfect(n), etc.

  • NumPy: Useful if you decide to generate generating arrays or grids of numbers for comparison.

3. Replicating the "Families" (The "Aplenty" Part)

The website categorizes numbers into families (e.g., "Happy Numbers," "Fibonacci Numbers," "Palindromic Numbers").

  • Standard Families: Checking if a number is a Fibonacci number or a Palindrome is trivial in Python (often 1–3 lines of code).

  • Exotic Families: For the more obscure types (like "Vampire numbers" or "Zigzag numbers"), you can look up their definitions (often found on the OEIS - On-Line Encyclopedia of Integer Sequences) and write a small function for each.

    • Example logic: "If the sum of the digits squared equals 1 (eventually), mark as Happy Number."

4. Customization (Your Requirement)

You mentioned wanting to add your own output. This is the biggest advantage of a custom script. You could design your program to:

  • Output data in a specific format (JSON, CSV, or a readable text report).

  • Run specific checks that Numbers Aplenty doesn't have.

  • Batch process hundreds of numbers at once (which would take forever on a web interface).

Summary of Feasibility

FeatureFeasibility in PythonDifficulty
Basic Properties (Odd, Even, Digits)Very HighTrivial
Number Theory (Primes, Factors)Very HighEasy (using SymPy)
Replicating 170+ FamiliesHighModerate (requires time to write checks for each)
SpeedHighFast for <20 digits; slower for massive numbers
Adding Custom OutputVery HighEasy

Conceptual Workflow

If you were to commission or build this, the program structure would look like this:

  1. Input: User enters a number (e.g., 1729).

  2. The "Checker" Engine: The number is passed through a list of functions:

    • check_prime(1729) -> False

    • check_taxicab(1729) -> True

    • check_harshad(1729) -> True

    • ...plus your custom checks.

  3. Output: The program prints a report combining the standard results with your custom metrics.

Verdict: Highly feasible. The "hard" part is just the manual labor of coding the definitions for the hundreds of specific number types you might want to track.

Getting Gemini to code in Python from scratch is much preferable to its revising code that I've written. I can gradually build up a list of functions that I want the program to execute in a Jupyter notebook. It will produce output similar but more extensive than Numbers Aplenty, it will not require Internet access, it will be efficiently coded and the code can be easily modified whenever necessary.

Monday, 4 August 2025

Counting Multiplicities in Partitions

I used a new LLM (z.ai) to help me convert Mathematica code to SageMath code and then explain how it all worked. The Mathematica code arose from one of the properties of the number associated with my diurnal age today: 27882. This number qualifies for membership in OEIS A184641.


 A184641: number of partitions of \(n\) having no parts with multiplicity 6.

The sequence begins as follows (permalink):

1, 1, 2, 3, 5, 7, 10, 15, 21, 29, 40, 54, 72, 96, 127, 166, 216, 279, 358, 457, 580, 735, 924, 1159, 1446, 1799, 2228, 2752, 3388, 4158, 5087, 6207, 7551, 9165, 11093, 13401, 16144, 19412, 23286, 27882, 33310, 39727, 47289, 56191, 66647, 78923, 93299

The Mathematica code is:

b[n_, i_] := b[n, i] = If[n == 0, {1, 0}, If[i < 1, {0, 0},
Sum[Function[l, If[j == 6, {l[[1]], l[[1]]}, l]][b[n - i*j, i - 1]], {j, 0, n/i}]]];
a[n_] := b[n, n][[1]] - b[n, n][[2]];
Table[a[n], {n, 0, 50}]

This is incomprehensible to me and so I got z.ai to change this code to SageMath code. I could still not understand the code generated and so I asked the following question:
I really don't understand this code. It doesn't make use of SageMath's Partition function which would seem necessary. Why is the use of this function avoided and is it possible to create more understandable code using this function. I really need to have this code explained step by step. My approach, which is admittedly lengthy, would involve using the Partition function to generate all the partitions for a particular value of \(n\) between 0 and 50. For each value of \(n\), I'd count how many partitions do not contain a part with multiplicity 6 and then move on to the next value of \(n\). Is this approach wrong-headed and too processor intensive. If so, then how can your approach be summarised in layman's terms (like I've done when describing my approach).
Here was the response in part:

This approach is not wrong-headed, but it could be processor-intensive for larger \(n\) because the number of partitions grows exponentially with \(n\). Here's a more understandable implementation using your approach:
def a(n):
    """
    Count the number of partitions of n where no part appears exactly 6 times.
    """
    if n == 0:
        return 1  # The empty partition
    
    count = 0
    # Generate all partitions of n
    for p in Partitions(n):
        # Get the multiplicities of each part
        multiplicities = p.to_exp()
        # Check if 6 is among the multiplicities
        if 6 not in multiplicities:
            count += 1
    return count

# Generate list of a(n) for n from 0 to 50
result = [a(n) for n in range(0, 51)]
print(result)

I then asked:

Can you explain the line "multiplicities = p.to_exp()".

The response was: 

The line multiplicities = p.to_exp() is a key part of the SageMath implementation that deserves a detailed explanation. Let me break it down step by step:

What p.to_exp() Does

    1. Partition Representation:

      • In SageMath, a partition p of an integer n is typically represented as a list of integers in non-increasing order (e.g., [5, 3, 2] for a partition of 10)
      • The .to_exp() method converts this into "exponential form"
    2. Exponential Form:

      • Instead of listing the parts, it lists the multiplicities (how many times each part appears)
      • The output is a list where:
        • The index position represents the part size (starting from 1)
        • The value at that position represents how many times that part appears

Example Walkthrough

Let's take a concrete example to understand this:

Partition: [4, 4, 3, 1, 1, 1] (which sums to 14)

Step-by-step conversion:

    1. Identify all parts and their counts:

      • Part 1 appears 3 times
      • Part 2 appears 0 times
      • Part 3 appears 1 time
      • Part 4 appears 2 times
      • Parts 5 and above appear 0 times
    2. Create the exponential form list:

      • Index 0 (part 1): 3
      • Index 1 (part 2): 0
      • Index 2 (part 3): 1
      • Index 3 (part 4): 2
      • Index 4+ (parts 5+): 0 (but these trailing zeros are omitted)
    3. Result: [3, 0, 1, 2]

Visual Representation

Partition: [4, 4, 3, 1, 1, 1]
Part sizes: 1, 2, 3, 4
Multiplicities:
Part 1: 3 times
Part 2: 0 times
Part 3: 1 time
Part 4: 2 times

Exponential form: [3, 0, 1, 2]

Why This is Useful for Our Problem

In our code, we want to check if any part appears exactly 6 times. With exponential form:

  1. We get a list of all multiplicities
  2. We can simply check if the number 6 appears in this list
  3. If 6 is in the list, it means some part appears exactly 6 times

More Examples

Example 1: Partition with a part appearing 6 times

  • Partition: [6, 6, 6, 6, 6, 6] (six 6's)
  • Exponential form: [0, 0, 0, 0, 0, 6]
  • 6 in [0, 0, 0, 0, 0, 6] → True (we exclude this partition)

Example 2: Partition without any part appearing 6 times

  • Partition: [5, 5, 5, 3, 3, 1]
  • Exponential form: [1, 0, 2, 0, 3]
  • 6 in [1, 0, 2, 0, 3] → False (we include this partition)

Example 3: Partition with multiple parts having different multiplicities

  • Partition: [7, 7, 4, 4, 4, 4, 2, 2, 2, 2, 2]
  • Exponential form: [0, 5, 0, 4, 0, 0, 2]
  • 6 in [0, 5, 0, 4, 0, 0, 2] → False (we include this partition)

Key Advantages of Using to_exp()

  1. Efficiency:

    • Avoids manually counting multiplicities for each part
    • Built-in SageMath function optimized for this operation
  2. Clarity:

    • Directly gives us the information we need (multiplicities)
    • Makes the check for multiplicity 6 straightforward
  3. Completeness:

    • Includes all part sizes from 1 up to the largest part
    • Even parts that don't appear (multiplicity 0) are represented

I then tried the to_exp() out and couldn't get it to work so I asked:

Why is it that when I run the SageMath code in SageMathCell: 
P=[7, 7, 5, 5, 5, 4, 3, 3, 3, 2, 1, 1, 1] print(P.to_exp()) 

I get the error message "AttributeError: 'list' object has no attribute 'to_exp'" 

The response was quite enlightening:

The error occurs because you're trying to call the to_exp() method on a Python list, but to_exp()is a method specific to SageMath's Partition objects. Here's the explanation and solution:

Why the Error Occurs

  1. Python List vs. SageMath Partition Object:
    • When you write P = [7,7,5,5,5,4,3,3,3,2,1,1,1], you're creating a standard Python list
    • Python lists don't have a to_exp() method - this method only exists for SageMath's Partition objects
    • The error message 'list' object has no attribute 'to_exp' tells you exactly this

How to Fix It

You need to convert your list into a SageMath Partition object first:

sage
# Create a Python list
P_list = [7,7,5,5,5,4,3,3,3,2,1,1,1]

# Convert to a SageMath Partition object
P_partition = Partition(P_list)

# Now you can call to_exp()
print(P_partition.to_exp())

Complete Working Example

sage
# Define the partition as a list
partition_list = [7,7,5,5,5,4,3,3,3,2,1,1,1]

# Convert to SageMath Partition object
p = Partition(partition_list)

# Now call to_exp() on the Partition object
multiplicities = p.to_exp()
print(multiplicities)

Expected Output

For the partition [7,7,5,5,5,4,3,3,3,2,1,1,1], the output will be:

[3, 1, 3, 1, 3, 0, 2]

Explanation of the Output

The output [3, 1, 3, 1, 3, 0, 2] represents:

  • Index 0 (part 1): appears 3 times
  • Index 1 (part 2): appears 1 time
  • Index 2 (part 3): appears 3 times
  • Index 3 (part 4): appears 1 time
  • Index 4 (part 5): appears 3 times
  • Index 5 (part 6): appears 0 times (not in the partition)
  • Index 6 (part 7): appears 2 times

Key Points to Remember

  1. Partition Objects vs. Lists:

    • SageMath has special Partition objects with extra functionality
    • Python lists are just basic data structures without these specialized methods
  2. Conversion is Simple:

    • Always convert your list to a Partition object first: Partition(your_list)
    • Then you can use all the SageMath partition methods like to_exp()
  3. Partition Requirements:

    • The list should represent a valid partition (non-increasing integers)
    • SageMath will automatically sort it if needed, but it's good practice to provide it in sorted order

Corrected Version of the Original Code

Here's how to fix the original code snippet you were trying to run:

sage
# Create the partition as a list
P = [7,7,5,5,5,4,3,3,3,2,1,1,1]

# Convert to Partition object
partition = Partition(P)

# Now to_exp() will work
print(partition.to_exp())

This will correctly output [3, 1, 3, 1, 3, 0, 2] without any errors. 

This whole exercise was very useful because it got me using this multiplicity function that I was completely unaware of. It's important to note that the Python list being converted to a SageMath partition is put into descending order. For example, consider the list P = [5, 6, 5, 4, 4, 7, 4, 3, 1, 1, 2, 3, 1]. Using "sorted" before the list we NOT work because the elements of the list are put in ascending order. What's needed is "reversed(sorted)" as shown below:

P=[5, 6, 5, 4, 4, 7, 4, 3, 1, 1, 2, 3, 1]
Q=Partition(reversed(sorted(P)))
R=Q.to_exp()
print(Q,"-->",R)

[7, 6, 5, 5, 4, 4, 4, 3, 3, 2, 1, 1, 1] --> [3, 1, 2, 3, 2, 1, 1]

However, even though the partition members must be arranged in descending, the output of the multiplicities is in ascending order. Thus we have three 1's, one 2, two 3's and so on. This can be a little confusing.

Here is another application of the to_exp() function, this time used to find the numbers of partitions of \(n\) that contain no multiplicities (OEIS A000009):

# Partitions of n that have no multiplicity higher than 1

L=[]
for n in [0..40]:
    P=Partitions(n)
    count=0
    for p in P:
        multi=p.to_exp()
        # Check if all multiplicities are <= 1
        if all(m <= 1 for m in multi):
            count+=1
    L.append(count)
print(L)

[1, 1, 1, 2, 2, 3, 4, 5, 6, 8, 10, 12, 15, 18, 22, 27, 32, 38, 46, 54, 64, 76, 89, 104, 122, 142, 165, 192, 222, 256, 296, 340, 390, 448, 512, 585, 668, 760, 864, 982, 1113] 

I'm thankful to z.ai suggestion of "if all(m <= 1 in multi)" because it's more succinct than my normal approach which is to set a variable OK to 1 and then reset it to 0 if a condition is not met. After that I test if OK is still equal to 1 and, if so, then I increment the count. This single line replaces the seven lines previously required:

OK=1
for m in multi:
    if m >1:
        OK=0 
        break 
if OK==1:
    count+=1

I need to remember that function for future use because my coding is ... well, terrible.