More from Victor Poughon
div.gradient { width: 650px; height: 60px; border-radius: 3px; margin-bottom: 10px; box-shadow: 5px 5px 5px rgb(0, 0, 0, 0.5); margin: 0 auto; } pre { max-width: 600px; } Colormaps are functions that map from [0, 1] to colors. I love colormaps! Maybe a little too much? There is a huge amount of very serious color science behind colormaps, and also a great Python package called colorcet which implements a lot of them. Would you ever need those as a CSS gradient? Probably not. Did I have fun writing a script to convert every 120 of them to a CSS gradient? Absolutely! Enjoy 😁 Note that these sample the actual colormap at N=101 points, and then your browser does linear interpolation to fill in the gaps. The actual colormaps are 255-entry tables. So not only is this horribly inefficient by using very verbose CSS syntax, it's also destroying a lot of the original colormap's information. This is just for fun, use the original data for anything serious. Also, I excluded the categorical colormaps because they don't really make sense as a gradient. Accent Blues BrBG BuGn BuPu CMRmap Dark2 GnBu Greens Grays / Greys OrRd Oranges PRGn Paired Pastel1 Pastel2 PiYG PuBu PuBuGn PuOr PuRd Purples RdBu RdGy RdPu RdYlBu RdYlGn Reds Set1 Set2 Set3 Spectral Wistia YlGn YlGnBu YlOrBr YlOrRd afmhot autumn bgy bgyw binary bjy bkr bky blues bmw bmy bone brg bwr bwy cet_CET_C1 cet_CET_C10 cet_CET_C10s cet_CET_C11 cet_CET_C11s cet_CET_C1s cet_CET_C2 cet_CET_C2s cet_CET_C3 cet_CET_C3s cet_CET_C4 cet_CET_C4s cet_CET_C5 cet_CET_C5s cet_CET_C6 cet_CET_C6s cet_CET_C7 cet_CET_C7s cet_CET_C8 cet_CET_C8s cet_CET_C9 cet_CET_C9s cet_CET_CBC1 cet_CET_CBC2 cet_CET_CBD1 cet_CET_CBD2 cet_CET_CBL1 cet_CET_CBL2 cet_CET_CBL3 cet_CET_CBL4 cet_CET_CBTC1 cet_CET_CBTC2 cet_CET_CBTD1 cet_CET_CBTL1 cet_CET_CBTL2 cet_CET_CBTL3 cet_CET_CBTL4 cet_CET_D1 cet_CET_D10 cet_CET_D11 cet_CET_D12 cet_CET_D13 cet_CET_D1A cet_CET_D2 cet_CET_D3 cet_CET_D4 cet_CET_D6 cet_CET_D7 cet_CET_D8 cet_CET_D9 cet_CET_I1 cet_CET_I2 cet_CET_I3 cet_CET_L1 cet_CET_L10 cet_CET_L11 cet_CET_L12 cet_CET_L13 cet_CET_L14 cet_CET_L15 cet_CET_L16 cet_CET_L17 cet_CET_L18 cet_CET_L19 cet_CET_L2 cet_CET_L20 cet_CET_L3 cet_CET_L4 cet_CET_L5 cet_CET_L6 cet_CET_L7 cet_CET_L8 cet_CET_L9 cet_CET_R1 cet_CET_R2 cet_CET_R3 cet_CET_R4 cet_bgy cet_bgyw cet_bjy cet_bkr cet_bky cet_blues cet_bmw cet_bmy cet_bwy cet_circle_mgbm_67_c31 cet_circle_mgbm_67_c31_s25 cet_colorwheel cet_coolwarm cet_cwr cet_cyclic_bgrmb_35_70_c75 cet_cyclic_bgrmb_35_70_c75_s25 cet_cyclic_grey_15_85_c0 cet_cyclic_grey_15_85_c0_s25 cet_cyclic_isoluminant cet_cyclic_mrybm_35_75_c68 cet_cyclic_mrybm_35_75_c68_s25 cet_cyclic_mybm_20_100_c48 cet_cyclic_mybm_20_100_c48_s25 cet_cyclic_mygbm_30_95_c78 cet_cyclic_mygbm_30_95_c78_s25 cet_cyclic_mygbm_50_90_c46 cet_cyclic_mygbm_50_90_c46_s25 cet_cyclic_protanopic_deuteranopic_bwyk_16_96_c31 cet_cyclic_protanopic_deuteranopic_wywb_55_96_c33 cet_cyclic_rygcbmr_50_90_c64 cet_cyclic_rygcbmr_50_90_c64_s25 cet_cyclic_tritanopic_cwrk_40_100_c20 cet_cyclic_tritanopic_wrwc_70_100_c20 cet_cyclic_wrkbw_10_90_c43 cet_cyclic_wrkbw_10_90_c43_s25 cet_cyclic_wrwbw_40_90_c42 cet_cyclic_wrwbw_40_90_c42_s25 cet_cyclic_ymcgy_60_90_c67 cet_cyclic_ymcgy_60_90_c67_s25 cet_dimgray cet_diverging_bkr_55_10_c35 cet_diverging_bky_60_10_c30 cet_diverging_bwg_20_95_c41 cet_diverging_bwr_20_95_c54 cet_diverging_bwr_40_95_c42 cet_diverging_bwr_55_98_c37 cet_diverging_cwm_80_100_c22 cet_diverging_gkr_60_10_c40 cet_diverging_gwr_55_95_c38 cet_diverging_gwv_55_95_c39 cet_diverging_isoluminant_cjm_75_c23 cet_diverging_isoluminant_cjm_75_c24 cet_diverging_isoluminant_cjo_70_c25 cet_diverging_linear_bjr_30_55_c53 cet_diverging_linear_bjy_30_90_c45 cet_diverging_linear_protanopic_deuteranopic_bjy_57_89_c34 cet_diverging_protanopic_deuteranopic_bwy_60_95_c32 cet_diverging_rainbow_bgymr_45_85_c67 cet_diverging_tritanopic_cwr_75_98_c20 cet_fire cet_gouldian cet_gray cet_gwv cet_isolum cet_isoluminant_cgo_70_c39 cet_isoluminant_cgo_80_c38 cet_isoluminant_cm_70_c39 cet_kb cet_kbc cet_kbgyw cet_kg cet_kgy cet_kr cet_linear_bgy_10_95_c74 cet_linear_bgyw_15_100_c67 cet_linear_bgyw_15_100_c68 cet_linear_bgyw_20_98_c66 cet_linear_blue_5_95_c73 cet_linear_blue_95_50_c20 cet_linear_bmw_5_95_c86 cet_linear_bmw_5_95_c89 cet_linear_bmy_10_95_c71 cet_linear_bmy_10_95_c78 cet_linear_gow_60_85_c27 cet_linear_gow_65_90_c35 cet_linear_green_5_95_c69 cet_linear_grey_0_100_c0 cet_linear_grey_10_95_c0 cet_linear_kbc_5_95_c73 cet_linear_kbgoy_20_95_c57 cet_linear_kbgyw_10_98_c63 cet_linear_kbgyw_5_98_c62 cet_linear_kgy_5_95_c69 cet_linear_kry_0_97_c73 cet_linear_kry_5_95_c72 cet_linear_kry_5_98_c75 cet_linear_kryw_0_100_c71 cet_linear_kryw_5_100_c64 cet_linear_kryw_5_100_c67 cet_linear_protanopic_deuteranopic_kbjyw_5_95_c25 cet_linear_protanopic_deuteranopic_kbw_5_95_c34 cet_linear_protanopic_deuteranopic_kbw_5_98_c40 cet_linear_protanopic_deuteranopic_kyw_5_95_c49 cet_linear_ternary_blue_0_44_c57 cet_linear_ternary_green_0_46_c42 cet_linear_ternary_red_0_50_c52 cet_linear_tritanopic_kcw_5_95_c22 cet_linear_tritanopic_krjcw_5_95_c24 cet_linear_tritanopic_krjcw_5_98_c46 cet_linear_tritanopic_krw_5_95_c46 cet_linear_wcmr_100_45_c42 cet_linear_worb_100_25_c53 cet_linear_wyor_100_45_c55 cet_rainbow cet_rainbow4 cet_rainbow_bgyr_10_90_c83 cet_rainbow_bgyr_35_85_c72 cet_rainbow_bgyr_35_85_c73 cet_rainbow_bgyrm_35_85_c69 cet_rainbow_bgyrm_35_85_c71 CET_C10 / circle_mgbm_67_c31 CET_C10s / circle_mgbm_67_c31_s25 cividis colorwheel cool coolwarm copper cubehelix cwr CET_C11 / cyclic_bgrmb_35_70_c75 CET_C11s / cyclic_bgrmb_35_70_c75_s25 CET_C5 / cyclic_grey_15_85_c0 CET_C5s / cyclic_grey_15_85_c0_s25 cyclic_isoluminant CET_C1 / cyclic_mrybm_35_75_c68 CET_C1s / cyclic_mrybm_35_75_c68_s25 CET_C9 / cyclic_mybm_20_100_c48 CET_C9s / cyclic_mybm_20_100_c48_s25 CET_C2 / cyclic_mygbm_30_95_c78 CET_C2s / cyclic_mygbm_30_95_c78_s25 CET_C8 / cyclic_mygbm_50_90_c46 CET_C8s / cyclic_mygbm_50_90_c46_s25 CET_CBC1 / cyclic_protanopic_deuteranopic_bwyk_16_96_c31 CET_CBC2 / cyclic_protanopic_deuteranopic_wywb_55_96_c33 CET_C6 / cyclic_rygcbmr_50_90_c64 CET_C6s / cyclic_rygcbmr_50_90_c64_s25 CET_CBTC1 / cyclic_tritanopic_cwrk_40_100_c20 CET_CBTC2 / cyclic_tritanopic_wrwc_70_100_c20 CET_C3 / cyclic_wrkbw_10_90_c43 CET_C3s / cyclic_wrkbw_10_90_c43_s25 CET_C4 / cyclic_wrwbw_40_90_c42 CET_C4s / cyclic_wrwbw_40_90_c42_s25 CET_C7 / cyclic_ymcgy_60_90_c67 CET_C7s / cyclic_ymcgy_60_90_c67_s25 dimgray CET_D4 / diverging_bkr_55_10_c35 CET_D6 / diverging_bky_60_10_c30 CET_D13 / diverging_bwg_20_95_c41 CET_D1A / diverging_bwr_20_95_c54 CET_D1 / diverging_bwr_40_95_c42 CET_D9 / diverging_bwr_55_98_c37 CET_D10 / diverging_cwm_80_100_c22 diverging_gkr_60_10_c40 CET_D3 / diverging_gwr_55_95_c38 CET_D2 / diverging_gwv_55_95_c39 CET_D12 / diverging_isoluminant_cjm_75_c23 diverging_isoluminant_cjm_75_c24 CET_D11 / diverging_isoluminant_cjo_70_c25 CET_D8 / diverging_linear_bjr_30_55_c53 CET_D7 / diverging_linear_bjy_30_90_c45 CET_CBD2 / diverging_linear_protanopic_deuteranopic_bjy_57_89_c34 CET_CBD1 / diverging_protanopic_deuteranopic_bwy_60_95_c32 CET_R3 / diverging_rainbow_bgymr_45_85_c67 CET_CBTD1 / diverging_tritanopic_cwr_75_98_c20 fire flag gist_earth gist_gray / gist_grey gist_heat gist_ncar gist_rainbow gist_stern gist_yarg / gist_yerg gnuplot gnuplot2 gouldian gray / grey gwv hot hsv inferno isolum CET_I1 / isoluminant_cgo_70_c39 CET_I2 / isoluminant_cgo_80_c38 CET_I3 / isoluminant_cm_70_c39 jet kb kbc kbgyw kg kgy kr linear_bgy_10_95_c74 linear_bgyw_15_100_c67 linear_bgyw_15_100_c68 CET_L9 / linear_bgyw_20_98_c66 linear_blue_5_95_c73 CET_L12 / linear_blue_95_50_c20 CET_L7 / linear_bmw_5_95_c86 linear_bmw_5_95_c89 CET_L8 / linear_bmy_10_95_c71 linear_bmy_10_95_c78 CET_L10 / linear_gow_60_85_c27 CET_L11 / linear_gow_65_90_c35 linear_green_5_95_c69 CET_L1 / linear_grey_0_100_c0 CET_L2 / linear_grey_10_95_c0 CET_L6 / linear_kbc_5_95_c73 CET_L20 / linear_kbgoy_20_95_c57 linear_kbgyw_10_98_c63 CET_L16 / linear_kbgyw_5_98_c62 CET_L5 / linear_kgy_5_95_c69 CET_L4 / linear_kry_0_97_c73 linear_kry_5_95_c72 linear_kry_5_98_c75 CET_L3 / linear_kryw_0_100_c71 linear_kryw_5_100_c64 linear_kryw_5_100_c67 CET_CBL1 / linear_protanopic_deuteranopic_kbjyw_5_95_c25 CET_CBL3 / linear_protanopic_deuteranopic_kbw_5_95_c34 CET_CBL2 / linear_protanopic_deuteranopic_kbw_5_98_c40 CET_CBL4 / linear_protanopic_deuteranopic_kyw_5_95_c49 CET_L15 / linear_ternary_blue_0_44_c57 CET_L14 / linear_ternary_green_0_46_c42 CET_L13 / linear_ternary_red_0_50_c52 CET_CBTL3 / linear_tritanopic_kcw_5_95_c22 CET_CBTL2 / linear_tritanopic_krjcw_5_95_c24 CET_CBTL1 / linear_tritanopic_krjcw_5_98_c46 CET_CBTL4 / linear_tritanopic_krw_5_95_c46 CET_L19 / linear_wcmr_100_45_c42 CET_L17 / linear_worb_100_25_c53 CET_L18 / linear_wyor_100_45_c55 magma nipy_spectral ocean pink plasma prism rainbow rainbow4 CET_R4 / rainbow_bgyr_10_90_c83 CET_R2 / rainbow_bgyr_35_85_c72 rainbow_bgyr_35_85_c73 CET_R1 / rainbow_bgyrm_35_85_c69 rainbow_bgyrm_35_85_c71 seismic spring summer tab10 tab20 tab20b tab20c terrain turbo twilight twilight_shifted viridis winter
OK, this is going to sound crazy: I believe all clocks are 30 seconds late. Let's get a few things out of the way: this is neither about time zones, nor leap seconds. It's not about synchronization of clocks, and it's not about relativity or any obscure corner of physics. I'm talking about everyday clocks. The ones that you might have around in your house, like this one that sits on my desk: Or even the clock on your phone! Maybe they're all correctly set very precisely to a very accurate reference clock, but here I will argue that they are still exactly 30 seconds late. In other words: It would be more accurate if they were thirty seconds ahead. What are you on about? All the above clocks have one thing in common: they don't show seconds. They truncate down to the nearest lower whole minute, right? So when it's actually 14:15:45, they'll show 14:15. And when the actual time goes from 14:15:59 to 14:16:00, then that's when your clock changes from 14:15 to 14:16. They apply the floor function! Ok great. Now hear me out! Let's compute the average error between a usual (truncating) clock, and the precise current time. import datetime import numpy as np import matplotlib.pyplot as plt import matplotlib.dates as mdates # A datetime array from 14:00:15pm for 300 seconds true_time = np.array([datetime.datetime(2024, 12, 20, 14, 00, 15) + datetime.timedelta(seconds=i) for i in range(300)]) # The clock truncates seconds shown_time = [t.replace(second=0) for t in true_time] # The clock error is the shown time minus the true time error = [dt.total_seconds() for dt in (shown_time - true_time)] fig, ax = plt.subplots(figsize=(10, 3.5)) ax.plot(true_time, error, label="clock error", color="teal") ax.hlines(np.mean(error), xmin=true_time[0], xmax=true_time[-1], label="average", linestyle="--", color="orange") ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M')) ax.set_ylim([-60, 60]) ax.set_title("A normal clock") ax.legend() Sometimes the error is 1 or 2 seconds, like when the clock shows 14:02 and it's actually 14:02:01 Sometimes the error is almost 60 seconds, like when the clock shows 14:02 and it's actually 14:02:57 So, the average error of a truncating clock is 30 seconds. Let that sink in for a moment: the average error of a clock is 30 seconds. If clocks rounded to the nearest minute instead of truncating, the average error would be 0. Therefore all clocks are 30 seconds late! Similarly of course, all clocks that show seconds but not milliseconds are half a second late. # The rounding clock! aka +30s shown_time = [(t + datetime.timedelta(seconds=30)).replace(second=0) for t in true_time] # The clock error is the shown time minus the true time error = [dt.total_seconds() for dt in (shown_time - true_time)] fig, ax = plt.subplots(figsize=(10, 3.5)) ax.plot(true_time, error, label="clock error", color="teal") ax.hlines(np.mean(error), xmin=true_time[0], xmax=true_time[-1], label="average", linestyle="--", color="orange") ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M')) ax.set_ylim([-60, 60]) ax.set_title("A rounding clock") ax.legend() This is just a convention But this is just a convention! Truncating, rounding or even ceiling are all valid, as long as we pick one and stick to it. By your logic we should say that it's Tuesday as soon as it's one second past noon on Monday. Well... kind of. It would be weird if we rounded for years, months and days, that's for sure. I think most people think of those scales as intervals. In other words, July is a period of time, with a start and an end. So are years, centuries, seasons. We are inside of it or outside. Is it the same for the current minute of the current hour ? I'm not so sure. Basically I'm arguing that rounding for clocks would be more useful than flooring. This is especially apparent when you're trying to calculate "how much time until my next meeting?", and your next meeting is at noon. If it's 11:55, you would usually mentally subtract and conclude: the meeting is in 5 minutes. That's how I always do it myself anyway! But the most probable estimate given the available information is actually 4'30"! If you had a rounding clock instead, 5 minutes would be the correct estimation. In fact, as we go down the various scales of time all the way from years, months, days and we reach hours, that's when we start truncating! At hours! For example: if it's 10:43 and you are asked the time and (for some context appropriate reason) you reply with just hours, you would say it's 11! Personally, I would never say that it's 10 if the clock shows anything past 10:30. I realize that's probably very different in other cultures, though! And I'd be interested to learn about how different cultures approximate time in language. So, looking at the various scales of time and how I mentally view them: Years: truncate. If the full date has 2019 in it, I'll say "the year is 2019" - all the way to the end. Saying "it's 2020" in September 2019 is super weird. Months: truncate. It's January all the way until it's February. Days: truncate. It gets a little weird if it's past midnight and I haven't slept yet. Is tomorrow today? Or is it the day after that? I think collectively we still haven't quite figured that one out yet. And that's ok. Kind of like how "next Friday" becomes ambiguous on Thursdays. Some cultures use times beyond 24:00, like 25:30 to indicate one hour and a half past midnight, and I think that's lovely! Hours: That's when mentally, I switch to rounding! At 15:48 I definitely feel like it's pretty much 16:00. Minutes: At 15:48:57, I definitely feel like it's 15:49! But clocks switch back to flooring for for some reason! But mentally we round! This is why I feel like all clocks are 30 seconds late: because the truncating convention doesn't match my intuition at the minute scale. Seconds and below: definitely rounding. Please someone tell me I'm not crazy 🙃 EDIT 2025-01-07: This kinda blew up on HN and I want to clarify something: This is a tongue-in-cheek article! It's fun to think about, but in the end, this IS a convention and the most important thing about conventions is that we all follow the same ones. I'm obviously not going to move all my clocks forwards by 30 seconds and neither should you! In many ways truncating is a much better convention and that's why we use it. I've also been made aware that Big Ben is truly an analog clock and doesn't jump around every minute, so I've removed that example. My bad! The point still stands because some "analog" clocks actually jump every minute and are not truly analog, but I should have picked a better example photo.
For my torchlensmaker project, I need to represent lenses surface shapes parameterically. That is, I need equations that give the X and Y coordinates of the surface, as a function of a parameter, usually called t. The project supports different parametric shapes, but a very popular shape in optics is the arc of circle. So far I had been using the usual polar coordinates parameterization: X=Rcos(t)+RY=Rsin(t) This works great, you can represent circles that cross the origin at t=0, and it works for both signs of R (the radius) to represent the two possible curvature directions: import numpy as np import matplotlib.pyplot as plt def half_circle(R, color): t = np.linspace(np.pi/2, 3*np.pi/2, 1000) X = R * np.cos(t) + R Y = R * np.sin(t) plt.plot(X, Y, color=color) plt.gca().set_aspect("equal") half_circle(10, "orange") half_circle(-10, "navy") However, I want to do numerical optimization to find the best possible shape for lenses. This representation has two problems when used for optimization: When R is very large, t gets very small for a similar sized arc. This means that the scale of the parameter can span over a large range of orders of magnitudes, which is difficult to optimize for and can lead to precision issues. Additionally, representing a flat vertical line is not possible, as R would be infinite. But a flat surface is a very common case in optics, and not handling it well is a problem. Crossing over and changing the direction of curvature of the circle arc during optimization is not possible with this representation, because R would have to "wrap around" positive infinity and come back to negative infinity. The solution I came up with is double: Use curvature instead of radius. Curvature is the inverse of the radius: K=1/R. This means a line is a circle of curvature zero. Brilliant! Don't use the angle as the parameter, but the Y coordinate directly. This reduces the shapes we can model to only half-circles, but for optics that's not a problem! So we know that the Y equation is easy: Y=t. But what about X? Well, I'll spare you the derivation, but here it is: X=Kt21+1−t2K2 And that's a half-circle 🤩! We can also get the derivative with respect to t, which is needed when doing collision detection with Newton's method (more on that in a future article maybe!) X′=Kt1−t2K2 import numpy as np import matplotlib.pyplot as plt def half_circle(R, color): t = np.linspace(-10, 10, 1000) K = 1/R X = (K * t**2) / (1 + np.sqrt(1 - t**2 * K**2)) Y = t plt.plot(X, Y, color=color) plt.gca().set_aspect("equal") half_circle(10, "orange") half_circle(-10, "navy") Interestingly, to get some help because I suck at calculus double check my work, I asked both an LLM and Wolfram Alpha to derive the equation. Wolfram Alpha comes out on top with the final form, while LLM gets stuck and can't simplify all the way. They both show step-by-step derivation (although Wolfram Alpha's is behind a paywall). ChatGPT4o mini (top, derivation not shown) vs Wolfram Alpha (bottom):
More in technology
On Sunday morning, a lot of us woke up to Grok Bot, the always-on AI assistant, decidedly off. “100% used” read my quota meter. It’d been 45% late the night before. It was an error, but of the Freudian slip sort, and that’s a problem if tools like this are to be taken seriously.
A Pika author was editing the same post in two browsers. They added six images in Browser A, then later they referenced an old tab editing the post in Browser B. That copy of the editor did not include those images. Browser B saved and removed all six images from Active Storage. A short time later, Browser A saved its newer copy of the post and put the image references back, but at that point the originally uploaded files were gone due to Browser B saving the post without the images. This exposed two separate problems: Missing images produced uncached 404 responses that the editor requested repeatedly. Action Text purged an image as soon as one save removed it from the body. Here’s how we fixed those problems. Why Action Text deleted the images Action Text keeps a rich text record’s embeds synchronized with the attachments in its body. When a submitted body no longer contains an image, Action Text removes its ActiveStorage::Attachment. Active Storage then purges the dependent blob, deleting both its database record and its file. That behavior makes sense when there is only one current copy of a document. It is unsafe when the same post can be open in multiple tabs, browsers, or devices. One stale save can delete an image that another editor is still using. Why the missing images flooded the server Browser A still had the six images in its editor, each pointing at an Active Storage redirect URL: /rails/active_storage/blobs/redirect/:signed_id/:filename Pika uses the ProseMirror-based Rhino editor. Rhino rebuilt an image’s DOM node when its caption or selection changed. Each new <img> element requested its URL again. Healthy images did not cause trouble since Active Storage makes its redirect cacheable. For a purged blob, Rails returned a 404 without Cache-Control, so the browser kept sending the request to Rails instead of reusing the failure. In a gallery, typing could re-request several missing images per keystroke. In these scenarios, this issue led to hundreds of requests hitting our servers. Fix 1: Cache permanent Active Storage 404s A purged blob’s redirect URL can never start working again. Blob IDs are not reused, and uploading the image again creates a new blob with a new signed URL. Sinc there is no concern of a 404 becoming stale, we made a specific 404 cacheable for the same period as a successful redirect: # app/controllers/concerns/active_storage_missing_blob_cache.rb module ActiveStorageMissingBlobCache extend ActiveSupport::Concern included do rescue_from ActiveRecord::RecordNotFound, with: :render_cacheable_not_found end private def render_cacheable_not_found expires_in ActiveStorage.service_urls_expire_in render file: Rails.public_path.join("404.html"), layout: false, status: :not_found end end We included the concern only in the controller serving these blob redirects: # config/initializers/active_storage_missing_blob_cache.rb Rails.application.config.to_prepare do ActiveStorage::Blobs::RedirectController.include(ActiveStorageMissingBlobCache) end (We also offered an update to Rhino that avoids it being so aggressive re-requesting images.) Fix 2: Detach dropped images instead of purging them When a save removes an image from an Action Text body, we now remove the attachment while temporarily keeping its blob. Another browser can save the image again and reattach that same blob. If nothing brings it back, a cleanup job purges it later. First, we added a timestamp for when the image was dropped: class AddDetachedAtToActiveStorageBlobs < ActiveRecord::Migration[8.1] def change add_column :active_storage_blobs, :detached_at, :datetime end end We then intercepted the private callback Active Storage uses to purge a dependent blob: # config/initializers/detach_dropped_embeds.rb module DetachDroppedEmbeds private def purge_dependent_blob if dropped_from_rich_text? blob&.update_columns(detached_at: Time.now) else super end end def dropped_from_rich_text? record.is_a?(ActionText::RichText) && !destroyed_by_association end end ActiveSupport.on_load(:active_storage_attachment) do prepend DetachDroppedEmbeds end The destroyed_by_association check preserves intentional deletion. Removing an image from a saved body detaches it, but deleting the post and its rich text still purges its images normally. Purge detached blobs after a grace period The cleanup query considers both when the blob was created and when it was detached: ActiveSupport.on_load(:active_storage_blob) do scope :unattached_since, ->(time) { unattached.where( created_at: ..time, detached_at: [nil, ..time] ) } end That scope handles both kinds of unattached blobs: A new upload that has never been attached becomes eligible based on created_at. An image removed from an old post becomes eligible based on detached_at. Pika gives both a 30-day grace period: class PurgeUnattachedUploadsJob < ApplicationJob GRACE_PERIOD = 30.days def perform ActiveStorage::Blob.unattached_since(GRACE_PERIOD.ago).find_each do |blob| blob.purge_later end end end If another browser saves the image during that period, it is attached again and the unattached scope excludes it from cleanup. The result Replaying the original two-browser edit now works safely: Browser B saves a body without the six images. The attachments are removed, but their blobs are only marked as detached. Browser A saves its newer body and reattaches the existing blobs. The images remain available and do not need to be uploaded again. If no editor brings a detached image back, the cleanup job eventually purges it. Much better! Reply by email
Yesterday, a group of MIT researchers released an article titled “Sparks of Weight-Space Programming: LLMs can natively read & write neural network weights”. The piece describes a set of experiments which convincingly show OpenAI’s latest Astra model is able to understand and successfully alter raw weight data. Wowzers! It’s a rough analogy, but an LLM’s weights are sort of like its brain. A set of weights is a large pile of numbers generated during training, and an LLM’s output is derived by running input through the weights using some matrix multiplication and a bit of randomization. While the steps to generate weights are well understood, the weights themselves are extremely opaque. There’s ongoing work to try and explain how weights work, but researchers currently have a limited understanding of what these numbers do. Based on this, I was quite surprised to learn that LLMs are able to act on raw weight data — even if it’s still just in limited cases. It’s unclear if OpenAI’s model was trained on weight data. Even if it was, I still find these results surprising. The article shows the model accurately predicting what a never-before-seen set of weights was built to do, fixing another model by updating its weights, and creating new sets of weights that performed simple tasks. I would not have guessed any of that would be possible a couple of days ago. This whole thing is slightly unnerving. The whole “weights as a black box” thing has always felt a bit sketchy to me, even when I was working with much simpler machine learning models. Now these experiments hint at a future where LLMs could be self-improving in ways that aren’t human-verifiable. Machines building machines, and we can only hope that they’re not doing anything weird. I don’t know if this strategy is scalable, but I’d guess that it’s something more people will be exploring very soon. More and more often these days, I wish we were living in less interesting times.
Harry threw me off course in episode 48 of Traction Heroes. At first, I thought was reading from a book about mind mapping. Soon, it became clear it was about something else: how to define a clear vision for a team or organization. Visions are notoriously hard to articulate. Drawing a picture — especially a picture of the whole forest, plus the trees — can help. The passage Harry read from Cameron Herold’s Vivid Vision explores this idea: So to visualize something a little more tangible, think about a tree trunk in the earth, inner core with its branches stretching out into the sky. Each of those branches contains twigs extend further out. The mind map works in a similar way, a core that branches out into smaller components. Now, apply this to the world of business. As the leader of an organization, you’re firmly planted at the center, and the various areas that contribute to the business branch out: marketing, sales, engineering, and so forth. With that in place, build off that by describing those branches in about three or four bullet points each. In other words, describe what your marketing looks like and sales and so on until you’ve captured every idea. Remember too that among the branches of your business, you want to consider every aspect of the organization: the vibe, informal codes of conduct, the fiscal environment, the scenery, all of that. Deliberately leave out numbers and data. Get a feel for the place. When you step out of your time machine three years from now and walk around the office, what’s happening? What can you say that is typical of the business three years from now? The mind map is an initial foray into the vivid vision and will act as an outline for the rough draft that you, the leader, will then write. I’ve sketched diagrams before when outlining articles, workshops, and presentations, never in service of aligning around a company’s vision. Harry’s done it several times and found it easier than trying to write it out. Seems useful — check out our conversation for more: Traction Heroes episode 48: Visions
Sharing a few data points because some people did not expect these drives to last that long.