AI - Circle Packing

florisdejonge's Avatar

florisdejonge

16 Apr, 2025 10:55 PM

This comment was split from the discussion: NodeBox and AI

EDIT: i split this comment in 2 parts

Hi John,

Thanks for starting this topic. I went ahead and tried to make a packing script. The results can be found in the attachments. It still has its quirks, so it probably could be improved upon, like a wrapper to make it more user-friendly, but this way it's a little more visible what is happening. I also found that the result is not static (rendering different nodes within the same branch, like before and after the groupings).

Here's the prompt I used as a starting point.

Can you generate a Python 2.7 script for circle packing for use in Nodebox 3 based on the documentation provided? It should have different sized circles packed within a certain rectangular bound. Don't use any external libraries. Please specify the type of input per argument (float, integer, etc.)

  1. Make arguments for X and Y, which are scattered point coordinates. The amount of points is the maximum amount of circles.
  2. Make an argument for padding: the space between the circles
  3. Make an argument for the size of the radii. This is a list of values within a range with steps of 10 between each value
  4. Make arguments for both the width and the height of the bounds.

Begin with the largest circles and end with the smallest circles. For each circle size, attempt to insert new circles in the spaces between until you hit the maximum amount of circles. Once the count limit is hit, move on to a smaller size.

Output a list of the necessary sizes in the same order as the points are which were used as input. This should create a properly packed arrangement of circles.

This a selection of what Claude came up with (I also did tests with Gemini and ChatGPT, but the results were less good). The first 2 fragments is the description of circle_packing.py script.

This enhanced algorithm uses a three-pass approach to maximize circle placement: First Pass
The algorithm attempts to place circles starting with the largest sizes, working down to smaller ones. This is similar to the previous version but serves as a foundation for the next passes.
Second Pass (Key Improvement)
For each point that doesn't yet have a circle:

It calculates the maximum possible radius that would fit at that point without:

Extending beyond the rectangular bounds Colliding with any existing circle

It then places the largest possible circle at that point (rounded to the nearest step size), even if it's smaller than what we initially tried in the first pass.

Third Pass As a final sweep, it tries to place the minimum-sized circles at any remaining points. This ensures we make use of even the smallest gaps.
Additional Improvements

Dynamic Sizing: The second pass is the major improvement, allowing circles to be sized exactly to fit available space rather than using predetermined sizes. No Wasted Points: By adapting circle sizes to the available space, many more points should get non-zero radii.
Boundary Awareness: The algorithm carefully checks edge conditions to prevent circles from extending beyond the width/height bounds.

Tips for Best Results

Input Point Distribution: The algorithm works best with a relatively even distribution of points. If you're using scattered points, try using a larger number with a good spread. Parameters:

Use a smaller step value (e.g., 2-5) for smoother size transitions Start with a smaller padding value (0-2) and adjust as needed
Make sure max_radius isn't too large relative to your bounds

The key differences in this aggressive approach:

Center-First Placement: The algorithm sorts points by distance from the center of the canvas and places circles starting from the center outward. This creates a more visually balanced result and often leads to better packing in practice. Direct Radius Calculation: For points without circles after the first pass, the algorithm directly calculates the maximum possible radius that can fit without any collisions, rather than trying to fit into predetermined size buckets.
Exact Sizing: This version places circles with exact calculated radii rather than rounding to the nearest step size. This ensures we're making the most efficient use of space.

The enhanced algorithm from Claude has resulted in the circle_packing_dense.py script

This enhanced algorithm includes several significant improvements to maximize packing density: Key Improvements:

Grid-Based Initial Placement: The algorithm starts by placing larger circles in a grid pattern. This creates a more even distribution of larger circles and avoids the common problem of random placement leaving awkward gaps. Space Score Ranking: Points are ranked by how much empty space surrounds them - prioritizing points in larger open areas for each radius size.
Adaptive Radius Selection: The algorithm dynamically adjusts circle sizes based on available space, maximizing the use of every potential point.
Multiple Strategic Passes: The implementation uses multiple passes with different strategies to ensure comprehensive packing.
Debug Information: Added output to show how many circles were successfully placed.

Tips for Best Results:

Parameter Tuning: Smaller padding: Try setting padding to 0 or a very small value (0.5-1.0) for maximum density
Smaller step size: Use a smaller step (2-3) between radius values for smoother size transitions
Appropriate min and max radius: Make sure the radius range makes sense for your canvas size

Point Distribution: Provide plenty of input points (2-4× more than the number of circles you expect)
A uniform distribution with slight randomness works best
Use a scatter node with a high amount setting

Canvas Size: Ensure width and height values make sense relative to your radius settings
A good rule of thumb: width and height should be at least 10× your max_radius

Floris

  1. Support Staff 1 Posted by john on 16 Apr, 2025 11:14 PM

    john's Avatar

    Floris,

    Circle packing is a GREAT idea - another one I've been meaning to tackle for ages.

    I haven't yet taken a close look at either circle_packing or circle_packing_dense. But before we go any further I made a few simple modifications that should save us both a little time (attached).

    • I changed the names of your two nodes to circle_pack and circle_dense so that the labels would be easier to read and distinguish.
    • I changed the Output Range of both nodes to List. This makes them output a list of separate numbers instead of a single array list, so you don't have to remove the square brackets and form the list yourself.

    I wish the Python code would handle the culling of dust itself, but if that proves difficult we could move that culling into a wrapper subnetwork. We could also either change the module or the wrapper to output diameters instead of radii (multiply by 2).

    But before we go there it seems like we need to do more fundamental thinking about the algorithm itself: what you should feed into it and what you should get back.

    As I said, I haven't even looked at this yet or done any research but I have two problems off the bat:

    1. I don't see much difference between the two variants. We ultimately need to pick just one (with maybe a density setting as a parameter).
    2. The output doesn't look very pleasing to me. It doesn't look as interesting or varied as other circle packing output I've seen. I'm not sure if that's a problem with the input or with the algorithm, but I think we need to do better.

    Thanks again for launching another AI adventure. I think this will make an excellent test case and probably another great node.

    John

  2. Support Staff 2 Posted by john on 17 Apr, 2025 05:54 AM

    john's Avatar

    Floris,

    I took a nap and then did a little research. Some thoughts.

    A few more problems with the current approach:

    • I don't think we should feed in a list of points. The whole point of the circle packing algorithms I'm seeing is that you give it some constraints and then the algorithm picks the points (and radii).
    • I definitely want to pack circles inside shapes other than rectangles - ideally ANY closed shape.

    In addition to the usual Wikipedia articles, I found two particularly good links that describe the kind of thing I'd like to do:

    https://mathematica.stackexchange.com/questions/40334/generating-vi...

    This starts with a gallery of all the things I'd love to be able to do, and then breaks the desired outcomes into multiple problems with algorithms and code snippets. Perfect!

    https://www.tylerxhobbs.com/words/a-randomized-approach-to-circle-p...

    An essay on this very topic from none other than Tyler Hobbs. He gives a rough sketch of the approach he uses.

    I have not yet studied this in depth, but my initial take-away is that we might want two nodes:

    • Node 1 (pack_full?) would seek to fill an arbitrary shape with as many circles as possible given a min circle size (target), max circle size (target), and minimum padding.

    • Node 2 (pack_values?) would seek to fill an arbitrary shape with a defined number of circles of proportional sizes.

    I gather this is not the kind of thing that can ever yield a precise final solution. Instead, you must iterate to some defined limit and then quit at that point. So we may also need to supply a time limit or limit on the number of iterations.

    So here is my first cut at parameters.

    Pack_full:

    • Boundary shape
    • Min Circle Size
    • Max Circle Size
    • Padding (in pixels)
    • Max iterations (higher means denser but slower)

    Pack_value:

    • Boundary shape
    • Values. A list of n values. Algorithm will return n circles with areas (not radii) proportional to each value. Actual size of circles will depend on what will fit within the boundary shape. In this mode the node will function like the treemap node. To achieve Tyler Hobbs' example you would just use repeat and combine nodes to build a list of three hundred 10s and ten 50s.
    • Padding
    • Max iterations

    In both cases, I think the node should return actual circles (ellipses). This will save us the bother of converting centroids and radii into circles ourselves.

    I really think having any closed boundary shape is possible and highly desirable (see the cool shapes in that first link). As a torture test I'd like to be able to fill any textpath (e.g. a lower case a) with circles. But if that proves too hard, we might fall back to, say, any polygon (closed shape with no curves), or even any convex polygon. That would still allow us to resample many arbitrary shapes and use, say, a 100-sided regular polygon to serve as a proxy circle for circle-within-circle packing.

    We might also consider adding a color list input to color the circles at some point as a convenience, but that could be handled in the wrapper subnetwork or done after the fact, so let's not worry about that for now.

    It might be possible to combine these two nodes into one, by only using min and max circle inputs if no value list is supplied. But I think these two functions may require different algorithms and asking Claude to do both at once might trigger some kind of AI uprising. Anyway, that's another thing that could be handled with a single subnetwork wrapper calling one of the two Python-based nodes.

    I'm not sure how unreasonable all this is. I do think it's possible but it may be a challenge getting Claude to meet these specs. My approach, however, is always user-centered: understand what the user needs and then build a solution to that, not do what's easiest for the computer and make the user adapt to that.

    Please look at these links and see if you agree with my proposed specs. Feel free to provide other use cases, links, or constraints. This might take even more than 15 iterations to get right. Are you up for the challenge?

    John

  3. 3 Posted by florisdejonge on 17 Apr, 2025 07:13 AM

    florisdejonge's Avatar

    Hi John,

    I'll get back to your second comment. But in regard to the first one and some of the questions you raised.

    • The right column in the example is more grid-like (which supposedly could lead to better packing).
    • On aesthetics: there are solutions in which the centre gets bigger circles and they get smaller towards the edges (similar to your attractor node).
    • On examples. I first tried to feed Claude these examples, but it only seemed to get confused. So I didn't add an example script. If we can find one, or a specific algorithm, I could try that (maybe there are in your second comment, I will take a look).
    • On using Claude: I did give it more documentation, based on these pages.

    Floris

  4. 4 Posted by florisdejonge on 17 Apr, 2025 03:50 PM

    florisdejonge's Avatar

    Hi John,

    I had some time to read and ponder you second response.

    I did come across Tyler Hobbs’ article beforehand and I did use some elements in my prompts, especially the steps of the algorithm. Based on this I also added an argument for maximum amount of attempts and I do have some prototypes of this scripts (the total amounts easily to 15 different ones btw), but Nodebox seemed to interpret this as frames. Therefore it was not a process within the algorithm that would give better results, so I scratched that.

    The suggestion to let the algorithm decide where to place the points, as illustrated in the other article, is interesting though. Especially dealing with packing in irregular shapes. Your suggestions for parameters help in that direction.

    In regard to the custom nodes and its output. I am still learning how they work. There seems to be a lot of possibilities hidden. It is possible to combine two algorithms in one script, I have seen the generated examples. But I think you have to call them in separate custom nodes, or is that what you can fix with a wrapper, with like a switch? In addition, I wasn’t aware one could output a list. But now your also suggesting the script could return the actual geometry, is that correct? Do you have an example of that - within your library perhaps? That would help in next attempts.

    Floris

  5. 5 Posted by florisdejonge on 17 Apr, 2025 06:49 PM

    florisdejonge's Avatar

    Hi John,

    Here is the result of another step which I took based on your suggestions. I think were making progress.

    This was the prompt I gave to Claude:

    Can you generate a Python 2.7 script for circle packing for use in Nodebox 3 based on the documentation provided? It would seek to fill an arbitrary shape with a defined number of circles of proportional sizes. I want to give it some constraints and then the algorithm picks the points and diameter for the circles. Don't use any external libraries. Please specify the type of input per argument (float, integer, etc.)

    1. Make an argument for the Boundary shape. This is a closed path.
    2. Make an argument for Values. This is a list of n values. The algorithm will return n circles with areas (not radii) proportional to each value. Actual size of circles will depend on what will fit within the boundary shape.
    3. Make an argument for padding: the space between the circles
    4. Make an argument for a maximum amount of attempts. The more iterations the packing grows denser, but might be slower
      Output a list of point coordinates in X and Y and a value for the diameter.

    And its response:

    How the algorithm works: It calculates a scaling factor to make the circle areas proportional to your values while fitting within the boundary
    Sorts circles from largest to smallest (this typically gives better packing results)
    For each circle, tries random positions within the boundary up to max_attempts times
    Checks that each position is inside the boundary and doesn't collide with existing circles
    Returns the positions and diameters of successfully placed circles
    The function outputs two lists: one with (x,y) positions and another with the corresponding diameters.

    It also gave the suggestion to add Force-directed packing:

    How Force-Directed Circle Packing Works The force-directed approach works like a physics simulation where each circle experiences various forces:
    Initial Placement: First, we use the basic algorithm to get an initial layout of circles.
    Force Calculation:
    Repulsive Forces: Overlapping circles push each other away
    Attractive Forces: A weak force pulls circles toward the center of the boundary
    Boundary Forces: Circles outside or near the boundary edge get pushed inward

    Position Update: For each iteration, we calculate these forces and update the circles' positions with a damping factor to avoid oscillation. Iterations: The process repeats for a set number of iterations, gradually settling into an optimized configuration.

    Benefits of Force-Directed Placement Better Space Utilization: Achieves more compact arrangements than the random placement approach
    More Natural Distribution: Creates visually appealing patterns that often look organic
    Adaptability: Works well with irregular boundary shapes
    Balanced Distribution: Circles tend to be more evenly spaced

    Implementation Notes The iterations parameter controls the optimization quality - more iterations give better results but take longer
    The damping factor prevents circles from moving too quickly
    Force magnitude limits prevent any single force from dominating the system
    The weak attractive force to the center helps prevent circles from getting stuck at the edges

    Now I am out of messages again. So this is the result for now. I still could not figure out how to elegantly make the output work better without clunky extra nodes. Looking forward to your response.

    Floris

  6. 6 Posted by florisdejonge on 17 Apr, 2025 06:52 PM

    florisdejonge's Avatar

    Something seemed to go wrong with the attached files. This is another try to upload them.

  7. Support Staff 7 Posted by john on 17 Apr, 2025 07:02 PM

    john's Avatar

    Hi Floris,

    I have to run in 3 minutes, so will read and respond later today.

    But you can look at contours.py for an example of how to return a path (like a circle). The trick is to import the path function from Nodebox:

    from nodebox.graphics import Path

    def contours(path):
    return [Path(c) for c in path.contours]

    In that case we pass a path and get a list of contours back.

    in convex_hull.py we import Point from Nodebox and return Nodebox points.

    Or look at the bounds function in image.py. Returns a rectangle path.

    make_curve.py returns a curve in a similar way. An ellipse is just four curves. I can't seem to find a case where I returned an ellipse, but I'm sure it's possible using a similar approach.

    Thanks for all your work. More later!

    John

  8. Support Staff 8 Posted by john on 18 Apr, 2025 04:22 AM

    john's Avatar

    Floris,

    We are indeed making progress!

    Attached is a revised version of pack_values that now seems to be working pretty well, with some improvements added to the Python code by hand:

    • Added a seed parameter
    • Node now returns circles

    I forgot to mention the seed parameter in my earlier spec. Just as with noise_loop the seed is essential; without it the node would return a different solution each time you hit reload. Fortunately it was easy to add by hand this time.

    It took some trial and error, but I finally figured out how to make the Python code return circles instead of centers and diameters. You can look in the code to see how I did it. The Path.ellipse function adds all the circles as separate contours in a single path so I had to separate out the contours before returning it. There is probably a cleaner way to do this, but the code works so I'm declaring victory.

    I also changed the node type in Nodebox to geometry (black) and added a comment.

    The attached demo shows a circle and a letter a as boundaries. I also added a heart filled with hearts to demonstrate that you can easily retrieve the centers and diameters from the circles if you need to.

    FURTHER THOUGHTS

    I also added circle counts below each boundary. In the current configuration I am sending a list of 200 values, so ideally there should be 200 circles (or hearts) each time. But as you can see, this is not always the case.

    At 2000 Max Attempts the circle boundary only held 199 circles. I was able to get the last missing circle by raising that value to 2500.

    The letter a, at 2000 Max Attempts, only contains 195 circles. You could get this up to 200 if you decrease padding to 0.

    The hearts only holds 175 circles, and that was even after I increased Max Attempts to 5000. I tried increasing Max Attempts to 10000 but that only added one extra heart.

    The moral of this story is that you are not guaranteed to have every value in your list represented. Depending on the boundary shape, seed, padding, and Max Attempts you may fall short, and in some cases there may be no way to cover all the supplied values.

    This is probably not too much of a problem for generative art (as long as you are aware of it). But for information visualization, it could be a serious shortcoming. One area of future research might be to ask Claude for an algorithm that could guarantee every value is represented (i.e. that the number of circles produces always equals the number of values supplied).

    You may also notice I removed the force-directed packing alternate node. I played with it for awhile and, despite Claude's insistence that it produces more aesthetically pleasing results, I couldn't really see that much difference. And it's more complicated with more parameters to confuse end users. If you don't like one arrangement from pack_values you can just keep changing the seed until you get something better. So that seems good enough to me - would you agree?

    The node is a little slower than I'd like; it starts to drag if you feed it more than a few hundred values. But that seems to be the nature of the beast, and a few hundred circles per boundary is enough to produce many interesting projects.

    NEXT STEPS

    I would still like to see a pack_full node (with the addition of a seed and the ability to return circles) that just needs min and max circle size and provides as many circles as needed to fill the available boundary. Pack_values gives you more control, but you have to adjust the number of values to fill a given shape in a pleasing way. It would be nice just to fill whatever shape you give it with no adjustments or value list needed.

    But if this is hard I think we could live with what we have now.

    As I said above, it would also be nice if the node could guarantee that every value was represented. I think it's worth asking Claude if he can do that. But again, we can do a lot with what we already have.

    Other than that, I think we should just play with the attached node and see if it really does meet our needs.

    I must say, I am again impressed by how quickly we arrived at a workable solution. I was afraid this might be too hard for Claude. He still needed help returning circles, but I can't really blame him for that.

    I look forward to your reaction!

    John

  9. 9 Posted by florisdejonge on 18 Apr, 2025 06:10 AM

    florisdejonge's Avatar

    Hi John,

    Thanks for the reply and the improvements. The seed was necessary as a requirement in my regard, you don't want to change the design when you close and open the file again, so its great you were able to add it. But the design/script still seems to reload every time, is that correct? That would make it impossible to replicate a design based on a seed number.

    If that would work as intended, I think the challenge has been completed. This result is for me good enough to use and has unlocked a lot of possibilities. I like the use case of the hearts by the way, it demonstrates nicely what also could be done. It's fast enough as far as I'm concerned, I have way slower networks.

    The force directed solution doesn't seem to add much visually indeed, but I got the impression from the description this could lead to animation applications (moving circles, which in my mind look like the network graphs from Gephi). That would be cool, but not plottable, and there are other possibilities for moving circles, so I am not that interested in that. The same accounts for data visualization applications, since I mostly use Nodebox for generative art/design.

    I do like the suggestion to see if a full_pack node is possible, since it could be a little easier in use. I have to take a good look at how geometry is generated in the script and how to call it from a custom node.

    As far as I am concerned, were going up to the next challenge (Voronoi?) or would you want to pursue fine-tuning this one?

    Floris

  10. 10 Posted by florisdejonge on 18 Apr, 2025 06:33 AM

    florisdejonge's Avatar

    I was worried about the seed function. So I asked Claude to fix it to maintain consistent across node selections. Looks like it did a good job. Attached is an updated script.

  11. Support Staff 11 Posted by john on 18 Apr, 2025 08:21 AM

    john's Avatar

    Floris,

    I'm a tad confused by the seed issue. I tested my version repeatedly and each seed number I tried reproduced the same pattern of circles. Changing to a different seed changed the pattern; changing it back again restored the previous pattern. I even tried quitting and relaunching Nodebox and comparing the patterns to the screenshot I took earlier.

    So I could not find a case where the pattern ever changed with the same seed. Did you find such a case? There's probably something I don't understand. Random numbers are notoriously tricky.

    But in any case, the new version seems to work just fine, If you and Claude are happy, then I am too.

    One thing. You attached the same zip file twice. The folder contained only packed_values_seed.py, but the ndbx file wanted both packed_values.py AND packed_values_seed.py and so wouldn't open until I edited the file. Also the name change of the .py file conflicts with the reference in the node comment. Madness reigns!

    I was also thinking we should change the name anyway, especially if we are going to declare victory and move on. So I'd like to change the name of the node (and the comment and the .py file) from pack_values to pack_circles. I only suggested pack_values to distinguish it from a second node (pack_full). But if we are only going to have one node, pack_circles will be more descriptive of what it actually does. The node packs circles, not values.

    SO, I went ahead and renamed everything. I will formally publish the node separately after a little more testing, But until then I suggest that everyone who wants to play with this use the version attached to THIS post:

    • The name of the node is now "pack_circles"
    • The name of the Python module is now "pack_circles.py"
    • The comment on the node now says that it "requires pack_circles.py"
    • The name of the Nodebox file is now "pack_circles_demo.ndbx"

    I even redid the screenshot. All attached.

    And yes, if you don't want to explore any more variants of circle packing then, by all means, feel free to tackle Voronoi next. Please start a new thread for that.

    And thanks again for ushering in the dawn of a new era!

    John

  12. Support Staff 12 Posted by john on 19 Apr, 2025 06:56 AM

    john's Avatar

    Hi Floris,

    I think we have a good working baseline circle packing node now. And I know I said feel free to move on in my last note. But I'm starting to play with it and now I think we might all benefit from a few more experiments.

    Two issues...

    1 - Reliable Output

    I still think it's an issue that the current node can't guarantee that you will get circles for all the values you give it. This is obviously a major problem for people doing data visualization; your response was that as a generative artist you weren't particularly interested in data visualization.

    Fair enough. But I'm already finding situations - purely as an artist - that not having a reliable count of circles is a hinderance. I want to play with different configurations of the same number of circles in the same container. But this breaks if those configuration lists don't have the same number of items. I want to be able to freely step through different seeds without having things break downstream. So I'm already seeing that not having a reliable output is a problem for artists as well.

    I am also just curious. How easy would it be for Claude to solve this problem? Would Claude have to switch to an entirely different approach? Would the resulting output still look the same aesthetically? Might it be even better? No way of knowing unless we try,

    2 - Absolute Boundaries

    One of the first things I thought of making was a bubble gum machine. Are you old enough to remember those? A bunch of colorful gumballls held inside a glass globe?

    But here's the problem. The current node defines containment as any circle whose center is inside the boundary. It doesn't matter if part of the circle pokes out beyond the boundary,

    This is fine for some applications. But sometimes I want my circles to respect the boundary absolutely. I don't want any gumboils sticking outside the globe. So again I wonder: how hard would it be for Claude to fix that problem? "Make a version of the current node where no part of any circle can protrude beyond the boundary".

    One way of doing this would be to add an extra test. If the shortest distance from a circle's center to any point on the boundary is less than the circle's radius, that circle is no longer considered inside.

    If Claude could do this, it would be interesting to see what effect it would have on the overall output. Would it make it even hard to achieve full coverage (see issue 1)? Would it cause a significant slowdown? Would the resulting packing patterns look noticeably sharper? No way of knowing unless we try.


    For both of these issues, if we get interesting results we could possibly package them as separate nodes inside a single wrapper subnetwork, with a check box that said "Absolute boundaries" or "Guarantee full coverage" or something.

    As I start to play more with circle packing I'm coming to realize that this is not an all-or-nothing thing. There are all sorts of different ways to pack circles, each with its own pros and cons. I don't think we've fully explored these possibilities yet.

    What do you think?

    John

  13. 13 Posted by florisdejonge on 20 Apr, 2025 10:11 AM

    florisdejonge's Avatar

    Hi John,
    I agree that we should explore possibilities further. After some testing making a penplot design, I found it takes quite some work-arounds to get the circles to actually not overlap with the boundaries. In addition, I think that giving it an exact number of circles or specific mininum and maximum sizes would be more intuitively.

    Floris

  14. 14 Posted by florisdejonge on 09 May, 2025 08:27 AM

    florisdejonge's Avatar

    Hi John,

    It took a while (I got sidetracked into other AI-penplot applications), but I continued the development of the circle packing node. You can find the example attached. It's based on your earlier demo. This file contains, aside from the earlier version, 3 new examples.

    The first one needs improved_pack_circles.py. This forces the circles within the boundaries. The argument 'ensure all fit' is a boolean, but I think it's on by default, so I don't think we actually need the control. This is what Claude gave as a description

    Boundary Respect: When placing circles, the script now ensures each circle (including its full diameter) stays within the boundary by:

    Adjusting the random position range to keep circles away from edges

    Checking 8 points around each circle's circumference to confirm full containment

    When a circle can't be placed after the initial attempts, the script continues trying with up to 5x the original max_attempts.

    The script still respects the circle proportions based on your input values.

    The 2nd and 3rd example needs improved_pack_circles2.py. It uses a fixed amount, which can be based on the input range or seperately set. I found the max_attemps ought to be set higher to get a similar packed result, making this a little slower. And again, some notes from Claude.

    Exact Circle Count Support: Added an exact_count parameter that ensures your script uses exactly that many values/circles.

    Automatic Size Adjustment: The script now automatically adjusts circle sizes if it can't fit all circles at the current scale:

    It starts with an initial sizing calculation. If not all circles fit, it reduces circle sizes by 15% and tries again. This process repeats up to 10 times until all circles can be placed

    Prioritizes Using All Values: By default, exact_count is set to the number of values you provide, ensuring the script tries to use all 200 input values.

    Looking forward to your respons,
    Floris

  15. Support Staff 15 Posted by john on 11 May, 2025 02:07 AM

    john's Avatar

    Floris,

    First of all, thanks for sticking with this. I think we are making steady progress and may in fact have reached our goal.

    I was a little confused by the demo. It wouldn't open because the original pack_circles.py, needed by the first of your four new examples, was not included. So I just disabled that example.

    Also the demo contained not just the four examples shown in your screenshot, but my previous three examples - except that those three had been modified to use improved_pack_circles. Took me a little while to figure all that out.

    So let's just focus on the last three of the four new examples shown in your screenshot, which I'll call example 2, example 3, and example 4.

    Let's see if I've got this right:

    • example 2 uses improved_pack_circles. All circles land within boundary but not all values supplied are guaranteed to be used.
    • example 3 uses improved_pack_circles2 with Exact Count set to 0. It looks like all circles still land within the boundary, but now all are guaranteed to be there.
    • example 4 also uses improved_pack_circles2 with Exact Count set to 100. Only the first 100 values are used.

    I played with these for awhile and came up with the following observations.

    ENSURE ALL FIT CHECKBOX

    For example 2 at first it looked like this had no effect, but if you get the right seed and padding you can find examples where it does make a difference. I thought that checking this box might reduce circles that were two closely packed, but the reverse is true: if you check this box it may add a few more circles that just barely fit the padding requirements (without affecting the previous circles picked).

    So I gather from Claude's comments that what this does is try even harder (up to 5x the original max_attempts). If you're lucky this might yield a few more circles in the current configuration. You can get a similar effect by just increasing the Max Attempts - though that would change the configuration.

    At first I thought this setting was not even used in improved_pack_circles2 since it always returned the exact number of circles requested. But I looked at the code and discovered that this setting DOES matter IF Max Attempts is less than about 10.

    Improved_pack_circles2 guesses at a circle size proportional to the values that it thinks will fit, and then reduces that by 15% each iteration. This means that if you only have a few iterations the circles may not shrink enough to fit. But after 10 iterations the size is already down to about 20% of the initial guess - which usually seems be enough. After 20 iterations the circles are less than 4%, at 30 iterations less than 1% of the initial guess size.

    This means that, for improved+pack_circles2, Ensure All Fit will probably never be needed if Max Attempts is set to anything over 20. Even at 1000 circles 20 iterations only takes a few seconds, and the results are rather poor, so I don't see much reason users would ever want to set Max Attempts to less than 20.

    Leaving this option checked does not seem to noticeably degrade performance, so it's safe to leave it checked.

    Conclusion: exposing this option is dubious for example 2, unnecessary for examples 3 and 4.

    EXACT COUNT

    Based on my experiments, here's what this seems to do:

    • if set to 0, is ignored and node just uses n = the number of values supplied in the list
    • if set to 1 up to n-1, simply does a slice of the values. So if you provide a list with 197 small values followsd by 3 large values, then set the exact count to 199, you will get 177 small circles and 2 large circles. If set to 190 you will get 190 small circles.
    • if set to a value > n, is ignored and just uses the n values from the list. This is consistent with the slice: if you slice 300 values from a list with 200 values, you will just get those 200 values. This would probably confuse users. "! asked for 300, why is it only giving me 200?"
    • if set to <0, starts slicing from the end. So -1 equivalent to 199, -2 equivalent to 198, etc.
    • if set to -n or less, generates an error message.
    • if values list not supplied, node returns nothing even if Exact Count is > 0.

    Conclusion: all this does is slice the value list, which is not terribly useful. I think it would be better if it served as an alternative to the value list. We could let users EITHER supply a value list (in which case Exact Count would be ignored) or skip the value list and just return a number equal sized circles defined by Exact Count. I could do this easily with the existing node by wrapping it in a subnetwork.

    MAX ATTEMPTS

    The number of attempts needed for a good result will depend on the container shape, the number of values, the padding and the seed. For example 2, more attempts may slightly increase the number of circles. For examples 3 and 4, more attempts may increase the size of the circles (wasting less space within the container).

    Of course, the higher you set this number, the longer the node will take.

    I found that a value between 1000 and 2000 max attempts produced a fairly decent packing in most cases. Cranking that to a higher value (like 10,000) took noticeably more time but rarely resulted in a significant improvement. Even in the rare cases where it produced a noticeably tighter fit for examples 3 and 4, you could get an equal improvement by just trying other seeds.

    For an example with 300 circles, 1000 took about 2.3 seconds; 2000 took about 3.8 seconds - on my fast MacBook Pro. (10,000 took 13 seconds.). Sometimes 2000 was slightly better, sometimes there was not much noticeable difference. My preference is to return results as quickly as possible so the user gets immediate feedback; if they are not happy they can always try upping the max attempts or changing the seed. And 1000 is a round number.

    The processing time becomes more problematic as the number of values increases. Even at only 1000 max attempts, packing a thousand circles took 35 seconds, long enough that some users would give up and assume the node had frozen. I am therefor inclined to limit the size of the value list to no more than 1000, or even lower if default max attempts was higher than 1000.

    Setting Max Attempts to 0 causes an error so that should not be allowed.

    Conclusion: the default value should be 1000. I wish we didn't even have to expose a fudge factor like this to end users, but there do appear to be cases where a tighter packing may be worth the extra time, so I do think we need to expose this choice to end users.

    THE BOTTOM LINE

    • I really like having all the circles fitting cleanly within the container and would never want to disable this feature even if it would make the node run slightly faster. So we should toss our earlier version.
    • I also really like getting the number of circles I asked for. Even with just a thousand attempts I find the packings for examples 3 and 4 were almost as good as the packings for example 2.
    • It is true that example 2 runs faster. If asked to pack 1000 circles, example 2 only took 10 seconds instead of 35 - but also only returned 759 circles.
    • Even if offered two versions, I would never use example 2; it's just too much of pain dealing with the unpredictable output. I also think offering two almost identical nodes would be confusing and force the user to figure out which one to use each time. Combining the two variants in one node would also be confusing and might run slower. I therefor lean towards just going with improved_pack_circles2

    MY RECOMMENDATION

    • Finalize on improved_pack_circles2.
    • Wrap it in a subnetwork.
    • Hide the Ensure All Fit checkbox and leave it checked when calling the module
    • Set default Max Attempts to 1000.
    • Set minimum Attempts to 1 to prevent error.
    • Change "Exact Count" to "Number of Circles" and move to top. Set minimum to 0.
    • Make value list optional. If not provided use Number of Circles instead. If provided ignore Number of Circles.
    • Limit value list to a maximum of 1000 values.
    • Limit maximum value for Number of Circles to 1000.

    To be clear, I can implement all of these recommendations using a wrapper subnetwork with no need to change the Python code any further.

    What do you think?

    If you have no objections, I will create the wrapper subnetwork and post a candidate for our final version. We can then play with that for a few days and see how we like it.

    I haven't even looked at Voronoi yet; I want to get this locked down first. But I am very excited to look at it soon!

    John

  16. 16 Posted by florisdejonge on 12 May, 2025 04:55 PM

    florisdejonge's Avatar

    John,

    Thanks for your reply and thorough analysis. Apologies for the trouble the file gave you, I just started building onto the previous demo.

    • I agree with that one would always prefer the clean fitting into the bounds, so that option can be a default and we can forget about the previous version.
    • The option to choose a value list or a number for the exact count is great
    • 1000 attempts sounds reasonable based on your explanation
    • Your recommendations for a default setting/limitation is a good idea. I expect that only more experienced users need more specific use cases, and those can just edit the settings within the wrapper, right?

    So, if you want to go ahead and finalise this one, I’d be happy. Please give it version number, just like you do with other nodes in your library. I only found the limitations of our first version after trying to actually make something with it. This might be the case for this one as well, so we might want to improve it in future.

    Looking forward to your response regarding the Voronoi node.

    Floris

  17. Support Staff 17 Posted by john on 12 May, 2025 07:26 PM

    john's Avatar

    Thanks Floris.

    Attached is a demo with a v2 version of our node. It takes the following parameters:

    • Boundary - The shape containing the circles
    • Values. A list of relative sizes (optional)
    • Total circles. If no values supplied, the number of uniform circles desired. Ignored if values list is supplied.
    • Padding. Minimum space around each circle (in pixels)
    • Max Attempts. The maximum number of attempts. More attempts may increase the size of the circles and increase density within the container.
    • Seed. Each seed generates a different arrangement. Some arrangements will have higher densities.

    The node returns the circles sorted from largest to smallest. So if you want to maintain an association between specific values and specific circles (as you would need to do in an information visualization), you will want to sort your values before submitting them.

    The demo is as before but adds a fourth container: a square. The square example is currently set to place 15 uniform circles. I also added a density score to all examples. The density is an approximation for shapes with curves and contours, but is close enough for our purposes.

    I wanted to get this out there ASAP so that you and others can play with it. I think this version is a clear improvement over our previous version, but I still have some concerns.

    Especially when you place uniform circles in a square or rectangle you can see that the densities are pretty low. In fact for rectangles the MAXIMUM density possible with the current algorithm is only 50%. For uniform circles, depending on the container and the number of circles, it is generally possible to get around 70% to 90% density, so there is still a lot of empty space here; the circles don't look fully packed. I am almost tempted to call this place_circles instead of pack_circles.

    I tried tweaking the algorithm to improve this (alternate code included in zip file). I was able to get the densities up a bit, but doing so caused the node to run noticeably slower. A modest improvement caused the render time for the whole demo to increase from about 3 seconds to 13 seconds - which I find unacceptable. And even with this change, the results vary wildly with the seed and are often still poor.

    The algorithm is actually a bit lame. It begins by making a very rough estimate of the container area by simply multiplying the boundary box by 50%. This is low even for a circle (which occupies 78% of a square) and very low for a rectangle. It then tries a guess based on this low density. If that doesn't work it reduces the size by a whopping 15%. So for a rectangle, if 50% density doesn't work, it then tries 42.5%, then 36.12%. That's usually enough.

    But when I increased the initial density and caused it to reduce density more slowly, this dramatically slowed performance, often with little apparent improvement. This is because most random scatterings used for the positioning are doomed to fail. You have to try many arrangements before even 50% density can work. If you start with even 60% density, the chances of success with most random arrangements falls dramatically, causing it to churn futilely.

    The strength of the current algorithm is that it is VERY general purpose and still always returns the number of circles requested. We are asking a lot from it. It must handle ANY weird container shape and ANY arbitrary list of relative values. Given all that, for tasks like filling a heart shape with lots of different sized hearts, I think it works pretty well.

    If we were to constrain the ask, I think it could do a better job.

    One thing we both like about the new version is we now have an option. If you don't need a list of relative values and just want a certain number of uniform circles, you can just omit the values list. But currently the algorithm works the same either way. If it knew we only need uniform circles, maybe it could take a different approach that would result in higher densities.

    Or maybe it could be more clever about the random scatter it uses for initial placement. If it used a more sophisticated distribution for its scatter, it might converge on better solutions much more quickly.

    Another possible way to improve density would be to add a second phase. Once it comes up with a set of circles that satisfies the requirements, maybe it could use that as a base level and then improve it. If you look at 15 circles in the square, as a human you could easily improve the packing by jostling the circles around a bit (so that they were more evenly distributed) and then gradually increase the circle size until the padding limit was reached. This would often make a significant difference, but is harder for a computer to do and may become too time intensive if there are many circles. It would be interesting to see how well Claude could do this.

    Another thing. Sometimes I don't care about having an exact number of circles of particular proportions and just want to completely fill the space.

    One way of doing that would be to add another option: "Add Circles". This would normally be 0, but if you increased it to, say, 12, the node could add 12 more circles - of any size - to fill the remaining space. It would find the biggest open space and then add s circle big enough to fill the space up to the padding limit. It would then find the next biggest space and add a second circle. The more circles you add, the smaller each new circle gets, but the less wasted space. You could keep increasing this value until the container was full enough to suit your taste.

    This would give you a lot of control. You could use a value list or just a fixed number of uniform circles to get a promising starting arrangement, and then fill in the rest from there. Or leave Add Circles at 0 if you need a specific number of circles.

    It's hard to know when we are done if we don't have a clear idea of exactly what we want. When I google for "circle packing art" I see many different things. Some our current node can do, others it cannot.

    Do you, Floris, have some specific effects you are trying to achieve? How well does this latest version do? Are there other specific things you want to do but still cannot achieve? Anyone else?

    Let's play with this version for a few days and then consider if we want to try for a v3 that would have better overall density and maybe an option to fill the space with more circles.

    Let the playing begin!

  18. Support Staff 18 Posted by john on 14 May, 2025 08:41 AM

    john's Avatar

    In the spirit of playing, last night I used our circle_pack node to make a quick visualization of world population by country from 1970 to 2022.

    The node worked perfectly! I loaded a CSV file, did a lookup on population, fed that into the values port and VOILA! An instant chart of 234 countries, which I was then able to label, color code, and even animate.

    I posted the animation on my Instagram account. I have also attached it here, along with a PDF. It's fun to zoom into the PDF and spot all the tiniest countries.

    So far so good.

Reply to this discussion

Internal reply

Formatting help / Preview (switch to plain text) No formatting (switch to Markdown)

Attaching KB article:

»

Already uploaded files

  • circle_packing_09042025.zip 11.6 KB
  • Schermafbeelding_2025-04-16_om_21.33.24.png 321 KB

Attached Files

You can attach files up to 10MB

If you don't have an account yet, we need to confirm you're human and not a machine trying to post spam.

Keyboard shortcuts

Generic

? Show this help
ESC Blurs the current field

Comment Form

r Focus the comment reply box
^ + ↩ Submit the comment

You can use Command ⌘ instead of Control ^ on Mac

Recent Discussions

17 May, 2025 10:24 PM
17 May, 2025 12:17 AM
14 May, 2025 08:41 AM
09 May, 2025 01:15 PM
07 May, 2025 03:57 AM

 

06 May, 2025 06:46 AM
06 May, 2025 02:15 AM
21 Apr, 2025 10:47 AM
21 Apr, 2025 10:41 AM
21 Apr, 2025 05:56 AM
19 Apr, 2025 07:17 PM