Just because I don't care doesn't mean I don't understand.
699 stories
·
3 followers

We Know Simple Fluids Can Flow. Turns Out, Some Can Fracture

1 Comment
Researchers thought that what enabled complex fluids to break apart was their elasticity. But a crack in a nonelastic simple fluid has them questioning that idea.

When pulled at 100 millimeters per second, a blend of hydrogen and carbon stretches. At 300 millimeters per second, the fluid breaks.

Adapted with permission from Phys. Rev. Lett. 136, 124002. Copyrighted by the American Physical Society.

Thamires Lima, a research professor in chemical engineering at Drexel University, studies the properties of thick, viscous liquids — think honey or molasses, though in a lab you’re more likely to find polypropylene or crude oil. Using a method called extensional rheology, Lima stretches liquids between metal plates to find the force that makes them flow.

A few years ago, she was conducting a test as part of a project in collaboration with the oil and gas company Exxon Mobil when she heard a short, sharp crack. “I thought it was the machine,” Lima said. But the crack came from the fluid that the machine was pulling: a gooey, black blend of hydrogen and carbon. Instead of stretching, the fluid had fractured.

Fractures are known to occur in certain elastic complex fluids, which can act like solids under certain conditions. But Lima was working with a nonelastic simple fluid. Even with almost no elasticity, it snapped apart under stress.

“Nobody expected that this would be possible in this kind of simple fluid because viscosity usually just rearranges the molecules,” said Arnold Mathijssen, a fluid physicist at the University of Pennsylvania. “You don’t expect it to crack. But it does, so I think that’s what’s really surprising.”

A Brittle Break

Lima stretched the liquid again and again to prove that the unexpected crack wasn’t a one-off. “Every time that she measured it, the material would break,” said Nicolas J. Alvarez, the professor of chemical engineering at Drexel University whose lab led the research. “It makes a loud pop. I mean, like you just took a rubber band and pulled it and stretched it and it snapped.”

Convinced the snap wasn’t a fluke, Lima and Alvarez used high-speed cameras to look at the phenomenon more closely. They realized that the break was essentially a “brittle fracture,” the kind you might see when you drop a dish made of glass or porcelain.

Brittle fractures happen to brittle solids, which have elasticity. Apply some stress to glass or porcelain and it deforms a very tiny bit, and then — if you don’t push it past its breaking point — it springs back to normal once the stress is removed. However, solids are never perfect. In most cases, a brittle solid will have a teeny, tiny defect — a crack at the scale of tens of nanometers. Once the solid is stressed past a critical point, it becomes energetically more favorable for the solid to grow the crack than to elastically store the stress. At that point, the crack grows catastrophically, rapidly breaking the solid apart.

Some complex fluids, called viscoelastic liquids, also have elasticity. For example, polymer melts — melted versions of the polymers in plastics — are made up of long chains of molecules, which become entangled with one another and increase the material’s elastic component.

In a 2016 Physical Review Letters paper, Alvarez and colleagues showed that complex fluids like melted polystyrene can fracture in the same way that solids sometimes do. “We just thought elasticity was something that was a prerequisite for such solid type of breaking, right?” Alvarez said. As a result, they theorized that elasticity was related to the fracture of liquids as well.

But the hydrocarbon blend that Lima was working with was a simple fluid. Simple fluids don’t store much elastic energy. And when they are pushed or pulled past their limits, they don’t usually bend or break — they flow.

So perhaps the old theory about what makes a liquid fracture is wrong. “If there is no elasticity in a problem, then how can you think about initiation or growth of a crack?” said Brato Chakrabarti, a physicist who works on fluid mechanics at the International Center for Theoretical Sciences in Bengaluru, India.

The cracking of the hydrocarbon blend made the researchers look back at the papers of Daniel D. Joseph, a mechanical engineer at the University of Minnesota. In 1995 and 1998, Joseph suggested that any liquid, regardless of how elastic it is, could fracture under a sufficient amount of tearing stress.

Alvarez wonders if the breaking point of a liquid is related not to a property like elasticity, but to something more fundamental to the liquid’s structure. “Maybe, just maybe, the thing that causes [certain] fluids to break … [is] somehow related to this cohesive energy that holds the molecules together,” he said.

A Burst Bubble

Simple fluids do have a way of relieving stress, no breaking required: They form intermolecular voids (bubbles) in a process called cavitation.

If the blades of a propeller spin rapidly in a simple fluid, for example, the fluid on one side of the blade can slosh much faster than the fluid on the other, leading to a drop in pressure on that side. This drop can cause the liquid to cavitate. Engineers work to avoid this, because once those bubbles collapse, they generate shock waves that can damage propellers and pumps.

In his papers in the ’90s, Joseph predicted that cavitation would allow simple fluids to fracture.

“If you think about what holds a fluid together, it’s cohesiveness, or the intermolecular interactions between the molecules,” Alvarez said. If you pull those molecules apart, you can create a bubble. Usually, viscous liquids stay cohesive when bubbles form, by changing shape around them. But if enough bubbles form in quick succession, they could theoretically crack a liquid like a pane of glass.

At Drexel, the researchers found that once a crack nucleates inside a simple fluid, it propagates extremely fast, precisely because the fluid is not elastic. “If you can get that nucleation event of the crack to begin, because there is no elasticity in the material, that crack can propagate as fast as physics will allow it,” Alvarez said.

In previous work on complex fluids, the Drexel researchers found that cracks in melted polystyrene propagate at approximately 0.07 meters per second. In their new study, Lima and colleagues showed that cracks propagate far more rapidly in the simple liquids they studied, reaching velocities of approximately 500 to 1,500 meters per second.

“That has something to do with the way that the material is able to dissipate energy,” Alvarez said. According to one hypothesis, in a complex fluid, energy is absorbed by the long chains of molecules as they break. But in a simple fluid, “there’s really nothing to slow that crack down,” he said.

This seems to affect the shape of the crack, which in complex fluids looks like the horn of a trumpet and in simple fluids looks like a crack moving through glass, the researchers found.

How To Crack a Liquid

Surprisingly, despite their different ways of cracking, both the complex fluids and the simple fluids that researchers tested tended to fracture at the same critical measure of stress: 2 megapascals. The researchers varied the temperature of the hydrocarbon blend —  a simple fluid — to change its viscosity and found that only the least viscous liquid they tested failed to fracture. The team observed that the critical stress level at which liquids fracture is proportional to their viscosity times the strain rate (how quickly they are being pulled or stretched apart and how the diameter of the liquid is changing).

The machine had a limit — albeit a high one — to how quickly it could move: 500 millimeters per second. “There are very few instruments comparable to ours,” Lima said. Lima thinks that potentially, if they had a machine that could pull on the liquids faster, they could fracture less viscous liquids like honey or even water.

In the future, Lima wants to use a more transparent liquid so she can capture the crack as it forms. She would also like to try freezing the surface of the liquid as soon as it snaps and to probe it using a high-resolution microscope that scans surfaces at a nanometer scale.

Alvarez is keen to explore simple fluids in the context of spinning materials into fibers — which can have applications in engineering and medicine. Fractures in fluids could also have implications for inkjet printing, brain injury protection, and soft robotics.

But Alvarez is most excited to learn what it means for a simple fluid to fracture in the first place. “[It’s] different than what we’ve been thinking about in the literature for a very long time,” he said.

Adblock test (Why?)

Read the whole story
jgbishop
5 hours ago
reply
This is so weird!
Raleigh, NC
Share this story
Delete

Python⇒Speed: 6× faster binary search: from compiled code to mechanical sympathy

1 Comment

How do you speed up computational Python code? A common, and useful, starting point is:

  1. Pick a good algorithm.
  2. Use a compiled language to write a Python extension.
  3. Maybe add parallelism so you can use multiple CPU cores.

But what if you need more speed? Consider the following real problem, one of the steps in scikit-learn’s gradient histogram boosting algorithm:

  • You have a large array of floating point numbers.
  • You want to assign them to the integer range 0-254, spread out evenly.

scikit-learn implements this by splitting up the full range of float values into 255 buckets, creating a sorted array of bucket boundaries, and then using binary search to choose the appropriate bucket for each value. The binary search is implemented in a compiled language, and it can run in parallel on multiple cores.

Recently, as part of my work at Quansight, and inspired by two posts by Paul Khuong, I sped up this implementation significantly. How? By making sure the code wasn’t fighting against the CPU.

In this article I’m going to walk you through that speed-up, demonstrated on a simplified example. Then I’m going to demonstrate a series of additional optimizations, with the final version running 6× faster than the original one.

It’s worth knowing that I will be speeding through mentions of many different low-level hardware topics: instruction-level parallelism, branch (mis)prediction, memory caches, SIMD, and more. This is only one article, it can only briefly introduce you to what’s possible, it can’t function as an in-depth tutorial. So I’ll talk about how you can learn more about these topics at the end of the article.

Read more...
Read the whole story
jgbishop
23 hours ago
reply
Pretty cool performance boosts.
Raleigh, NC
Share this story
Delete

Pickles - 2026-06-30

1 Comment
Read the whole story
jgbishop
12 days ago
reply
Haha!
Raleigh, NC
Share this story
Delete

Ferrari reveals its first EV, with design help from Jony Ive

1 Comment and 2 Shares
An image of a blue Ferrari with a minimalist design and black accents.
The Ferrari Luce will start at €550,000 in Italy, but US pricing hasn’t been announced. | Image: Ferrari

After months of teasers, Ferrari is offering the first full view of its Luce electric vehicle. The Luce is notable not just for being Ferrari's first EV, but for being designed in collaboration with Jony Ive and Mark Newson at their collective LoveFrom. It's also going to be Ferrari's second four-door car and its first five-seat one.

We already knew Ive and Newson were working on the Luce's interiors, which were shown off earlier this year. Now Ferrari says LoveFrom was allowed to "define the design direction of the project from the outset," inside and out.

Tim Stevens reporting for Engadget offers a few firsthand impression …

Read the full story at The Verge.

Read the whole story
jgbishop
48 days ago
reply
This thing is *ugly*...
Raleigh, NC
Share this story
Delete

Brevity - 2026-04-30

1 Comment
Read the whole story
jgbishop
73 days ago
reply
What a deep cut reference!
Raleigh, NC
Share this story
Delete

Django: fixing a memory “leak” from Python 3.14’s incremental garbage collection

1 Comment

Back in February, I encountered an out-of-memory error while migrating a client project to Python 3.14. The issue occurred when running Django’s database migration command (migrate) on a limited-resource server, and seemed to be caused by the new incremental garbage collection algorithm in Python 3.14.

At the time, I wrote a workaround and started on this blog post, but other tasks took priority and I never got around to finishing it. But four days ago, Hugo van Kemenade, the Python 3.14 release manager, announced that the new garbage collection algorithm will be reverted in Python 3.14.5, and the next Python 3.15 alpha release, due to reports of increased memory usage.

Here’s the story of my workaround, as extra evidence that reverting incremental garbage collection is a good call.

Python 3.14’s incremental garbage collection

Python (well, CPython) has a garbage collector that runs regularly to clean up unreferenced objects. Most objects are cleaned up immediately when their reference count drops to zero, but some objects can be part of reference cycles, where some set of objects reference each other and thus never reach a reference count of zero. The garbage collector sweeps through all objects to find and clean up these cycles.

Python 3.14 changed garbage collection to operate incrementally. Previously, a garbage collection run would sweep through all objects in one go, but this could lead to “stop the world” stalls where your program’s real work could pause for seconds while the garbage collector did its job. The incremental garbage collection algorithm instead does a fraction of the work at a time, spreading out the cost of garbage collection.

Here’s the full release note (historical source):

Incremental garbage collection

The cycle garbage collector is now incremental. This means that maximum pause times are reduced by an order of magnitude or more for larger heaps.

There are now only two generations: young and old. When gc.collect() is not called directly, the GC is invoked a little less frequently. When invoked, it collects the young generation and an increment of the old generation, instead of collecting one or more generations.

The behavior of gc.collect() changes slightly:

  • gc.collect(1): Performs an increment of garbage collection, rather than collecting generation 1.
  • Other calls to gc.collect() are unchanged.

(Contributed by Mark Shannon in 108362.)

The problem

I’d been helping one of my clients upgrade to Python 3.14 for a few months, chipping away at compatibility work like upgrading dependencies and fixing deprecations. Tests were finally all passing and everything was working on the local development server. The next stop was to launch a temporary deployment using Python 3.14 via Heroku’s review apps feature.

At the basic tier, Heroku review apps use fairly resource-constrained servers, including just 512MB of RAM, with the ability to temporarily burst up to nearly 1GB (200%). Paying for larger servers is an option, but unfortunately the next step up is pretty expensive.

When I launched a review app for my Python 3.14 branch, I found its release phase failed while running migrate. Inspecting the logs, I found the migrations started fine:

$ heroku logs --app example-python-314-wsgk3w --num 1000 | less
...
app[release.6634]: System check identified no issues (26 silenced).
app[release.6634]: Operations to perform:
app[release.6634]: Apply all migrations: admin, auth, contenttypes, ...
app[release.6634]: Running migrations:

…but partway through, these messages started appearing:

heroku[release.6634]: Process running mem=527M(101.5%)
heroku[release.6634]: Error R14 (Memory quota exceeded)

…ramping up until the 200% mark:

heroku[release.9599]: Process running mem=977M(190.3%)
heroku[release.9599]: Error R14 (Memory quota exceeded)

…and finally the termination of the release process:

heroku[release.9599]: Process running mem=1033M(201.7%)
heroku[release.9599]: Error R15 (Memory quota vastly exceeded)
heroku[release.9599]: Stopping process with SIGKILL

These messages came from Heroku’s process management layer, which terminated the memory-hungry release process with SIGKILL after the hard threshold of 1GB memory usage was breached. Repeat attempts hit the same issue.

I was confused: migrations should not consume much memory. While they create a lot of temporary objects (Django model classes and fields) in order to calculate the SQL to send to the database, such objects are all short-lived and should be garbage-collected fairly swiftly. Additionally, migrations worked fine on the local and CI environments, and they’d never had memory issues on previous Python versions.

It looked like there was a memory leak, and it was time to dig in.

Initial investigation

I first profiled memory usage of migrate locally using Memray, the memory profiler that I covered in my previous post, using:

$ memray run manage.py migrate

The profiles revealed that memory usage had slightly increased on Python 3.14 compared to 3.13, but did not find a memory leak (a pattern of continual growth). Still, I made some optimizations to defer some imports, saving about 30% of startup memory usage, and tried again, to no avail.

I then had the idea to profile on a Heroku dyno directly. After hacking the release process to not run migrations, I built a review app and SSH’d into its web server:

$ heroku ps:exec -a example-python-314-rspwtc --dyno web.1 bash
Establishing credentials... done
Connecting to web.1 on ⬢ example-python-314-rspwtc...
~ $

Initially, I tried using Memray’s live mode to profile the migrations as they ran:

$ memray run --live manage.py migrate

While this tool looks great for some situations, it didn’t really work here, especially since it seized up after Heroku terminated the server.

I then tried running the default memray run command:

$ memray run manage.py migrate
Writing profile results into memray-manage.py.724.bin

…then, on my local computer, I repeatedly ran this command to copy down the results file:

$ trash memray-manage.py.724.bin && heroku ps:copy -a example-python-314-rspwtc --dyno web.1 memray-manage.py.724.bin

I was a bit worried here that the Memray binary file might be corrupted due to copying it while memray run was generating it. But with a final truncated copy left over after the server crashed, I asked Memray to generate a flamegraph for it:

$ memray flamegraph memray-manage.py.724.bin

…and it worked! Kudos to the Memray team for making their output format usable even when incomplete.

This more detailed flamegraph revealed more than 50% of the memory usage was allocated in ModelState.render(), which creates temporary model classes:

class ModelState:
    ...

    def render(self, apps):
        """Create a Model object from our current state into the given apps."""
        ...
        return type(self.name, bases, body)

This information hinted that these temporary model classes were hanging around beyond their expected short lifetime, leading to the memory leak. For example, every model class could also end up in a list intended for debugging, but accidentally extending the lifetime of these temporary classes.

I decided to dig a bit deeper using machete-mode debugging, with the below snippet that captures the temporary model classes and logs details about them. I wrote this within the Django settings file, where it was guaranteed to run at Django startup time, before the migrate management command.

import atexit
import gc
import tracemalloc
import weakref
from itertools import islice

from django.db.migrations.state import ModelState

tracemalloc.start(2)

orig_render = ModelState.render

rendered_classes = weakref.WeakSet()


def wrapped_render(*args, **kwargs):
    cls = orig_render(*args, **kwargs)
    rendered_classes.add(cls)
    return cls


ModelState.render = wrapped_render


@atexit.register
def show_referrers():
    print(f"🎯 {len(rendered_classes)} classes referred to.\n")

    for cls in islice(rendered_classes, 2):
        print(f"🎁🎁🎁 {cls!r} 🎁🎁🎁")
        for i, referrer in enumerate(gc.get_referrers(cls), start=1):
            print(f"🍌 Referrer #{i}: {referrer!r}")
            if tb := tracemalloc.get_object_traceback(referrer):
                print("\n".join(tb.format(most_recent_first=True)))
            print()
        print()
        print()

Note:

  1. tracemalloc.start() starts Python’s built-in memory allocation tracking.
  2. The ModelState.render() method was monkeypatched with a wrapper that stores every temporary model class in a WeakSet.
  3. The @atexit.register-decorated function runs at the end of the program, and logs two things.
  4. The first piece of logging is the number of temporary model classes still alive at the end of the program, which should be close to zero. (Some may stick around from the final migration state.)
  5. The second piece of logging iterates over the first two live temporary model classes and logs their name and their referring objects, discovered via gc.get_referrers(). For each referring object, it also logs the traceback of where that object was allocated, using tracemalloc.get_object_traceback() (which is why tracemalloc.start() was needed at the beginning).
  6. The emojis are a bit of fun to make the log messages easier to skim through. I have no idea why I picked 🎁 and 🍌!!

The output from this hook was voluminous, even with the limit to the first two live classes. For example, here’s the output for a temporary ContentType model class:

🎁🎁🎁 <class '__fake__.ContentType'> 🎁🎁🎁
🍌 Referrer #1: <generator object WeakSet.__iter__ at 0x1234ef300>
  File "/.../example/core/apps.py", line 45
    for cls in islice(rendered_classes, 2):

...

🍌 Referrer #11: {'name': 'model', ..., 'model': <class '__fake__.ContentType'>}
  File "/.../.venv/lib/python3.14/site-packages/django/utils/functional.py", line 47
    res = instance.__dict__[self.name] = self.func(instance)
  File "/.../.venv/lib/python3.14/site-packages/django/db/models/fields/__init__.py", line 1210
    self.validators.append(validators.MaxLengthValidator(self.max_length))

I checked the live referrers for a few classes, and they all seemed to be expected. However, it did reveal just how many cycles exist between ORM objects. For example, model classes refer to their field objects, which in turn refer back to their model classes, thanks to Django’s Field.contribute_to_class() creating this reference:

def contribute_to_class(self, cls, name, private_only=False):
    ...
    self.model = cls
    ...

Anyway, from comparing the output between Python 3.13 and 3.14, I could see that no new references were being created on Python 3.14. It seemed likely that the incremental garbage collection algorithm was the culprit.

The workaround

Given the investigation, I wanted to work around the issue by forcing a full garbage collection sweep with gc.collect() after each migration file ran. I came up with the below code, saved as management/commands/migrate.py in one of the project’s Django apps. It extends the default migrate command to run gc.collect() after each successful migration (where “apply” is forwards and “unapply” is backwards).

import gc

from django.core.management.commands.migrate import Command as BaseCommand


class Command(BaseCommand):
    """Extended 'migrate' command."""

    def migration_progress_callback(self, action, migration=None, fake=False):
        """
        Extend Django’s migration progress reporting to force garbage
        collection after each migration. This is a workaround to keep memory
        usage low, especially because we have a low limit on Heroku. It seems
        the incremental garbage collector introduced in Python 3.14 cannot
        keep up with the migration process’s tendency to create many cyclical
        objects, so our best fallback is to force collection of everything
        after each migration is applied or unapplied.

        https://adamj.eu/tech/2026/04/20/django-python-3.14-incremental-gc/
        """
        super().migration_progress_callback(action, migration=migration, fake=fake)
        if action in ("apply_success", "unapply_success"):
            gc.collect()

It felt a bit hacky, but it did the trick! The review app succeeded to launch, showing a flat memory profile as before.

We then continued to deploy to staging and production without any issues, and the team have been happily using Python 3.14 for over a month now.

Fin

Well, that’s where the tale ends right now. After the incremental garbage collection algorithm is reverted in Python 3.14.5, I guess I’ll be able to remove this workaround.

While it would be nice to have incremental garbage collection work well, it’s clear that the current implementation has some issues. I think the core team is making the right call reverting it, but hopefully there will be energy to improve the feature for the future.

May your garbage be collected efficiently and without fuss,

—Adam

Read the whole story
jgbishop
83 days ago
reply
What a niche bug! Good to know that this exists, however...
Raleigh, NC
Share this story
Delete
Next Page of Stories