Skip to content

feat: Add Lorenz attractor example#26

Merged
ali-ramadhan merged 7 commits intoali-ramadhan:mainfrom
denehoffman:lorenz-example
Aug 16, 2025
Merged

feat: Add Lorenz attractor example#26
ali-ramadhan merged 7 commits intoali-ramadhan:mainfrom
denehoffman:lorenz-example

Conversation

@denehoffman
Copy link
Contributor

Adds a Lorenz attractor example similar to https://docs.makie.org/stable/. This is intended to close #22.

@denehoffman
Copy link
Contributor Author

Let me know if you'd prefer to have visible axes, I removed them because I didn't like the look. Also wasn't sure how/if you wanted this added to the README.md, and your .gitignore doesn't allow mp4s so I wasn't sure if you wanted me to upload the result as well.

Copy link
Owner

@ali-ramadhan ali-ramadhan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @denehoffman! Thank you for contributing and closing an open issue. And congrats on being the first contributor to matplotloom!

This looks great and makes for a great example! I just left a couple of comments.

Also wasn't sure how/if you wanted this added to the README.md, and your .gitignore doesn't allow mp4s so I wasn't sure if you wanted me to upload the result as well.

Ah yes I would add it to the README.md like how the others have been added. I do have *.mp4 in .gitignore so I don't accidently commit output animations, but for the example animations I git add --force.

PS: Apologies for the very slow review. I was not honestly not expecting any PRs but will be watching this repo more closely from now on!

Comment on lines 35 to 68
@dataclass
class LorenzPlotter:
plot_speed: int = 20
attractor = Lorenz()
points: list[tuple[float, float, float]] = field(default_factory=list)

def initialize(self, steps: int):
self.points = [self.attractor.position]
for _ in range(steps):
self.attractor.step()
self.points.append(self.attractor.position)

@property
def frames(self) -> list[int]:
return list(range(1, len(self.points) // self.plot_speed))

def get_frame(self, i: int, loom: Loom):
fig, ax = plt.subplots(figsize=(12, 8), subplot_kw={'projection': '3d'})
points = np.array(self.points[: i * self.plot_speed])
xs, ys, zs = points.T
segments = np.array([points[:-1], points[1:]]).transpose(1, 0, 2)
norm = Normalize(vmin=0, vmax=len(xs))
colors = plt.get_cmap('inferno')(norm(np.arange(len(xs) - 1)))
lc = Line3DCollection(segments, colors=colors, linewidth=0.5)
ax.add_collection3d(lc)
ax.set_xlim(-30, 30)
ax.set_ylim(-30, 30)
ax.set_zlim(0, 50)
ax.view_init(
azim=(np.pi * 1.7 + 0.8 * np.sin(2.0 * np.pi * i * self.plot_speed / len(self.frames) / 10)) * 180.0 / np.pi
)
ax.set_axis_off()
ax.grid(visible=False)
loom.save_frame(fig, i - 1)
Copy link
Owner

@ali-ramadhan ali-ramadhan Aug 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense to make Lorenz a dataclass but LorenzPlotter feels like it shouldn't be. I would suggest just defining a plot_frame function but curious what you think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, now that I look back at the code, I can see why I did it this way. The attractor is a set of points, but the animation steps through them at each update, so to plot a specific frame, you'd have to know all the previous points. You could just generate all the points up to that frame on every frame, but that seems unnecessary, especially if we can just calculate them ahead of time and let the animation part just choose which ones to display. I could rewrite it to be a function which takes a list as an input, and add a function to Lorenz which spits out the desired number of points, if you'd like that let me know. Until then, I'll just add the animation to the README!

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fair! I usually run the full simulation then make plots and animations. Having thought about it a bit more I like your approach as it also shows a different way of using matplotloom compared to other examples. So let's keep it like this!


@dataclass
class LorenzPlotter:
plot_speed: int = 20
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think plot_speed can be given a more intuitive name. It sounds like when plot_speed = 1 then every simulation iteration is plotted, and plot_speed = 20 means that every 20th iteration is plotted.

Would steps_per_frame or frame_skip be better names?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think my original thought was that a higher value makes the plot spin faster, but I could probably think of a better way to phrase it, I think steps_per_frame makes sense.

@denehoffman
Copy link
Contributor Author

I've added the MP4 to the repo but I need to figure out how to link it in the README. I haven't used assets on a repo I don't own before, if you have any hints let me know!

@ali-ramadhan
Copy link
Owner

I've added the MP4 to the repo but I need to figure out how to link it in the README. I haven't used assets on a repo I don't own before, if you have any hints let me know!

Ah sorry I had to remind myself how I did it, but basically I wanted to host the animations on GitHub and have them show up in the README. But directly linking didn't work. So the workaround I found was to upload/attach them to an issue then the direct link from that could be pasted into the README. So I've been doing this in #11

It's debatable whether want to also include the animations in the examples/ directory as it could be seen as git repo pollution, but for now to be consistent let's include lorenz.mp4.

@denehoffman
Copy link
Contributor Author

denehoffman commented Aug 16, 2025

Ah sorry I had to remind myself how I did it, but basically I wanted to host the animations on GitHub and have them show up in the README. But directly linking didn't work. So the workaround I found was to upload/attach them to an issue then the direct link from that could be pasted into the README. So I've been doing this in #11

If I get the link by right-clicking the video there, it looks like https://private-user-images.githubusercontent.com/20099589/478727107-69b02d78-386d-4843-90df-b1a43600cfa5.mp4?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NTUzODcxNDYsIm5iZiI6MTc1NTM4Njg0NiwicGF0aCI6Ii8yMDA5OTU4OS80Nzg3MjcxMDctNjliMDJkNzgtMzg2ZC00ODQzLTkwZGYtYjFhNDM2MDBjZmE1Lm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA4MTYlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwODE2VDIzMjcyNlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTg0ZDVmMWI1YzUzZTMyYmY1ZTRkNTc2MTNkMjM3MGJjNDRkYzc1ZjRmZjBjNTkxNzk1YjJlZjYxOTgwNTNhOWUmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.lO4Bq05uw0Agcmv8ZPJRy2edLykOAsc-oePy0ze0W00 which seems to work, but doesn't look like the other links in the README. How are you getting that link exactly?

@ali-ramadhan
Copy link
Owner

Ah I'm not sure if there's another way but once it's uploaded by dragging and dropping into the text box, it turns into a link (which confusingly does not have an .mp4 extension). To get it back, I can edit the comment and copy the link directly from the text box:

image

I just realized you may not have be able to do this since I uploaded the movie, sorry about this!

@ali-ramadhan
Copy link
Owner

PS: Hope I'm not overstepping but I went ahead and just modified the README a little bit to move the Lorenz example into the section with the other examples. And I forgot to mention that we should add it to the docs so I did that too.

@denehoffman
Copy link
Contributor Author

Not overstepping at all, thanks for handling that!

@ali-ramadhan
Copy link
Owner

Awesome, I think this is good to merge now! I'll just open a new PR to tag v0.9.1.

Thank you again for your contributions!

@ali-ramadhan ali-ramadhan merged commit e073617 into ali-ramadhan:main Aug 16, 2025
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Lorenz63 example?

2 participants