From 831567b78cbe755fa6fb301595c1345454c87a99 Mon Sep 17 00:00:00 2001 From: Bruno Conde Kind Date: Fri, 2 Aug 2024 13:25:38 -0300 Subject: [PATCH 1/9] Docs covering steps of the life plugin --- docs/assets/life_toad.gif | Bin 0 -> 2997 bytes docs/life_pt1.md | 315 ++++++++++++++++++++++++++++++++++++++ docs/life_pt2.md | 126 +++++++++++++++ mkdocs.yml | 3 + 4 files changed, 444 insertions(+) create mode 100644 docs/assets/life_toad.gif create mode 100644 docs/life_pt1.md create mode 100644 docs/life_pt2.md diff --git a/docs/assets/life_toad.gif b/docs/assets/life_toad.gif new file mode 100644 index 0000000000000000000000000000000000000000..6caabb52f86059d821af8a1f2b2785c1924354bc GIT binary patch literal 2997 zcmeHJ`#%#39NneleSJve(XQ8+Ro*g$N(mK}UMg)1nMbBBGI=*#qIo~Z9!#^vyz>ZA z+wLezml4urHjlgsHCI31zu?~M{PO+f{Bq9cd(P)LIG(bA!Vv%jU=09hZ*Sk)+5!Lo zhK7cAc6P3=t}qx3g+fI~M`N*AJRVP{)3-Ujy}e^&WApR#+sqy8IPjlwKx0el+)1~K zCyu$EG(K!7`pdR3jrCms zUgr$yn4_k;ZwB}+(Gs_qIdz}%*Hg#q9$Ai;_?PjnjgdN*^@8`beeVvWxUUKZE1U+| zidIUrp=MNFzfQ7QpnIF2KvHb8e%7t?(wh0}WDXV45TVLc@6K67B;}g;jdnNIOg>6q zagVn~>yI-N&g=5fP0PL=<(T^T*`c6-^cqOt=(7T!yERq1ORZ`ffeY=O#X=CydSy1d z3wI=Hq~HPSU0=W*ri_JU@L1255Ix|53k&NT=>r0LB3^7vGd3fOT-sWHY|4yCaeCUK zqGE)*S-@x!u|%ctLyA7XiArubz~VO5za6+DC(D9iM6AULk2c^>;c-BHmc~Ovc1hF z@yQnSnpftTbI4MRYpdQwh(s&LE6G0OG%wev74s?Izg_^ccP2o!vfa5lcd)<5f<4fo zvU!tXHiKXu-m^)UpZAD7_=RA!npZ-22(m28Nj{atFHP!4B1!3D639Z_h9iZHM_3{W zS=np~rHHJOMQV`}N)+8JC~ER0qdS&)6)mf5a(H`Fxfi{}+|j=RD&t3^v-U~oh4q6W z0X1ftYc$lr@yQw5AmhqR?TC9v@fkkw$oxpTUb(9(AsD zZysbWbfI-Ruzg7gQEl0+AdTwa%XH6o$N^_iuQ)2hMNMn&wAoj^`olq;%6*$imP)wu zp^m+`b?;^G55G~>rJ5mK^XE6>_aL@u4vqT8!1m!%&+r0dxz~Qru7AFB!cOP^O!f}` zuMYfM>-?UgT~jTx@3EGxjoM1>^m|Gp12nu*H$7+{RPZc7)eP3sr19kItkv?TFT1&8p4q=c2{)NmG|xswZ#N?4nN0bMQw4S{n%Howo0 zW7tfC&6@2&D5i%$u>v0$jpSwxe2iHQC{vGJPum5L+U>;r6f6TuB*ZAmn-OmQ>a7fS z2J-f4CNN&QalI7t2jCYWHl;<0^Jf*u-AsrU+*?O1Lx~u$J{^ml&Nx>JJ>g`x>;~dKrOm#PR^@#%B2L?bJ38;%^`by%A$B|Y9V7$*&o&> z>*QDU2ISydo02-@N9*&LE~(@A7VvAxpe|bF=w&G?>cd;~RP|a+vroQCwiUR6&+hW3 ze&bOq?dL`EK@mb(h`>`I?>~nV1zrzqkhW5#KcBU^LGO5<{t@+BDdR{`@sHz$hD>?j pK^hYf{(X))?kzpSpoDbadj(3MRg;Yk{7o2fU#UxPt+z|u>Tf158%Y2F literal 0 HcmV?d00001 diff --git a/docs/life_pt1.md b/docs/life_pt1.md new file mode 100644 index 0000000..23c1804 --- /dev/null +++ b/docs/life_pt1.md @@ -0,0 +1,315 @@ + +# ??.1 Well... + +> "No." - _Doom Slayer_ + +Well, someone can, probably. But doom in a dataframe would be kinda hard to play, so let's try something simpler. +[Conway's Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) is a notorious Cellular Automaton that we could perhaps implement with a plugin. +For science, of course. + +![Toad pattern with period = 2](assets/life_toad.gif) + +Jokes aside, life allows us to show how a plugin can access elements in both neighbouring rows and columns for each element. +With a little bit of extra Python, we can display things in an almost pretty manner. + +> __Note:__ For this tutorial, we'll assume you created a new plugin from the cookiecutter template and named it `game_of_life` (these steps aren't shown here, since they were already covered at the very beginning of this series). + +In this section we'll cover the developer side of the plugin (both Python and Rust). +In the next section we'll show how a user can import and use what we developed here. + +## The Python side + +Let's take a look at what we'll implement first, in `game_of_life/__init__.py`: + +```python +import fileinput +from collections import OrderedDict +from itertools import tee, islice +from os import PathLike +from pathlib import Path +from typing import Iterable, Any + +import polars as pl +from polars._typing import IntoExpr + +from game_of_life.utils import register_plugin + + +# Parse a board from a file or stdin +def parse_board(ifile: str | ...) -> list[list[int]]: ... + +# Transpose a list of lists +def _transpose(board: list[list[int]]) -> list[list[int]]: ... + +# Creates a DataFrame from a list of lists +def board_to_df(board: list[list[int]]) -> pl.DataFrame: ... + +# Helper function to help us deal with corner cases +def _nwise_wrapping(iterable: Iterable[Any], n: int): ... + +# Advance the simulation by n steps +def step(df: pl.DataFrame, n: int = 1): ... + +# Register our plugin +def life_step(left: IntoExpr, mid: IntoExpr, right: IntoExpr) -> pl.Expr: ... +``` + +Starting with the function to parse a board from a file or stdin: + +```python +def parse_board( + ifile: str + | bytes + | PathLike[str] + | PathLike[bytes] + | Iterable[str | bytes | PathLike[str] | PathLike[bytes]], +) -> list[list[int]]: + """ + Converts a board in a file containing only 0s and 1s, e.g.:: + + 0010 + 0100 + + into: + [[0010],[0100]] + """ + return [ + [c for ch in ln if (c := int(ch)) in [0, 1]] + for line in fileinput.input(ifile) + if len(ln := line.strip()) > 0 + ] +``` + +Next, we have transpose. Why do we need it, anyway? Because the way a dataframe reads our list of lists is counter-intuitive when constructing it from a dict comprehension. +If we start with an input board like: + +``` +0000 +1111 +``` + +without transpose, we'd end up with: + +``` +>>> import polars as pl +>>> board = [[0,0,0,0],[1,1,1,1]] +>>> pl.DataFrame({f"c{idx}": row for idx, row in enumerate(board)}) +shape: (4, 2) +┌─────┬─────┐ +│ c0 ┆ c1 │ +│ --- ┆ --- │ +│ i64 ┆ i64 │ +╞═════╪═════╡ +│ 0 ┆ 1 │ +│ 0 ┆ 1 │ +│ 0 ┆ 1 │ +│ 0 ┆ 1 │ +└─────┴─────┘ +``` + +Not what we expected _visually_, so we transpose the initial board to have the resulting dataframe match it. + +```python +def _transpose(board: list[list[int]]) -> list[list[int]]: + return [[row[idx] for row in board] for idx in range(len(board[0]))] +``` + +Next one is `board_to_df`, which calls `_transpose` and constructs the DataFrame in a similar way to the example above. +The padding detail is just to avoid columns with larger names than others, feel free to ignore it: + +```python +def board_to_df(board: list[list[int]]) -> pl.DataFrame: + """ + Converts a list of lists of integers (0s and 1s) to a Polars DataFrame. + The inner lists must have the same length. + """ + + # This is done because each row will become a column - the user likely + # expects a dataframe that *visually* matches the input file + board = _transpose(board) + + padding_len = len(str(len(board) - 1)) + board_t_dict = {f"{idx:0{padding_len}}": row for idx, row in enumerate(board)} + return pl.DataFrame( + board_t_dict, + ) +``` + +Let's skip `_nwise_wrapping` and `step` for now and jump straight to the last function - we'll return to the two we skipped soon: + +> Don't forget to read the comments! + +```python +def life_step(left: IntoExpr, mid: IntoExpr, right: IntoExpr) -> pl.Expr: + """ + This is the function that registers the polars plugin. To use it directly, + data must be in the correct format. An interesting way to do so is to use + the same column names as the original data frame, so the resulting df will + have the same shape. See how this is done in the `step(df, n)` function. + """ + return register_plugin( + args=[left, mid, right], + lib=lib, + symbol="life_step", + is_elementwise=False, + ) +``` + +Ok, plugin registered. How do we use it? We create columns in `step` with `with_columns`. +And we do so in a way that the new columns will have the exact name as the previously existing ones, so they're overridden. + +But wait, there's something we didn't talk about. +What happens at the border of the board (both vertically and horizontally)? +Do we stop the simulation from propagating there, do we wrap around, or something else? +Many implementations stop the simulation at the border, so let's do it differently, let's wrap around! + +Wait, why are we talking about this here - isn't this a concern to be solved by our plugin in Rust? +Yes, but in Python-land is where we name our columns. +So in order to have that nice overriding behavior, we need to address it here. +This is also a hint at what the mysterious `_nwise_wrapping` function does: + +```python +def _nwise_wrapping(iterable: Iterable[Any], n: int): + """ + Returns overlapping n-tuples from an iterable, wrapping around. This means + the result will have the same length as `iterable`. It also means the first + element(s) will include elements from the end of the iterable, and + likewise, the last element(s) will include elements from the start, e.g.:: + + fn('ABCDE', 3) -> 'EAB', 'ABC', 'BCD', 'CDE', 'DEA' + """ + elements = list(iterable) + to_be_wrapped = elements[-(n - 2) :] + elements + elements[: n - 2] + iterators = tee(to_be_wrapped, n) + return [ + list(z) for z in zip(*(islice(it, i, None) for i, it in enumerate(iterators))) + ] +``` + +The implementation might look a bit complicated, but the docstring should clarify its goal. + +Now we're only missing `step`, which takes a DataFrame already in the expected format and returns another DataFrame with our plugin applied `n` times to it: + +```python +def step(df: pl.DataFrame, n: int = 1): + """ + Takes a df and returns df.with_columns(...) corresponding to `n` advanced + steps in the simulation + """ + padding_len = len(str(df.width - 1)) + + # colnums: [['{n-1}', '00', '01'], ['00', '01', '02'], ['01', '02', '03'], ... ] + colnums = _nwise_wrapping([f"{idx:0{padding_len}}" for idx in range(df.width)], 3) + + # colnames: ['00', '01', '02', '03', ... , '{n-1}'] + colnames = [cols[1] for cols in colnums] + + # colvalues: [, ... ] + colvalues = [life_step(*tuple(cols)) for cols in colnums] + + for _ in range(n): + df = df.with_columns(**OrderedDict(zip(colnames, colvalues))) + return df +``` + +We're done with the Python side of things. +And if you're wondering: "what plugin did we actually register with `life_step`" - +you're totally right to be confused, we didn't touch Rust yet! +Why did we leave it for last? +Because surprisingly, it's much simpler than the Python side, and much shorter too. + +## Let's get rusty + +What do we need to do? +For each element, we need to look at the the sum of the 8 neighbours, then apply the rule to decide whether the element will be dead or alive in the next iteration. +Here's what our entire `src/expressions.rs` looks like: + +```rust +#![allow(clippy::unused_unit)] +use polars::export::arrow::legacy::utils::CustomIterTools; +use polars::prelude::*; +use pyo3_polars::derive::polars_expr; + +#[polars_expr(output_type=Int64)] +fn life_step(inputs: &[Series]) -> PolarsResult { + let (ca_lf, ca_curr, ca_rt) = (inputs[0].i64()?, inputs[1].i64()?, inputs[2].i64()?); + + /* + We're "counting" on the user not to append or modify the DataFrame created + from the board file. + + In general, this might sound insane, but for our Game of Life, this is not + so unreasonable. + */ + let lf = ca_lf + .cont_slice() + .expect("Expected input to be contiguous (in a single chunk)"); + let mid = ca_curr + .cont_slice() + .expect("Expected input to be contiguous (in a single chunk)"); + let rt = ca_rt + .cont_slice() + .expect("Expected input to be contiguous (in a single chunk)"); + + let len = lf.len(); + + let mut out: Int64Chunked = ca_curr + .into_no_null_iter() + .enumerate() + .map(|(idx, val)| { + // Neighbours above + let prev_row = if 0 == idx { + lf[len - 1] + mid[len - 1] + rt[len - 1] + } else { + lf[idx - 1] + mid[idx - 1] + rt[idx - 1] + }; + + // Curr row does not include cell in the middle, + // a cell is not a neighbour of itself + let curr_row = lf[idx] + rt[idx]; + + // Neighbours below + let next_row = if len - 1 == idx { + lf[0] + mid[0] + rt[0] + } else { + lf[idx + 1] + mid[idx + 1] + rt[idx + 1] + }; + + // Life logic + Some(match (val, prev_row + curr_row + next_row) { + (1, 2) | (1, 3) => 1, + (0, 3) => 1, + _ => 0, + }) + }) + .collect_trusted(); + out.rename(ca_curr.name()); + Ok(out.into_series()) +} +``` + +Awesome, now what? If we ignore tests, _as plugin developers_, we could say we're done. +Nothing's happened yet, so how could we be done? +In the next section we'll take a look at how the plugin _user_ would call the functions we made available. + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/life_pt2.md b/docs/life_pt2.md new file mode 100644 index 0000000..5f4b3d1 --- /dev/null +++ b/docs/life_pt2.md @@ -0,0 +1,126 @@ + +# ??.2 Plugin user + +In the last section we saw what the plugin developers made available for a plugin user. +Now we put the user's hat and demonstrate that _usage_. +For this, we'll implement a CLI app that will parse a board file provided as an argument, then run a step of the simulation every `delay` seconds (also provided as an argument). + +> Tip: place the code in this section in a separate file, e.g., `run.py`. + +Just like what we did previously, let's look at an overview of what's to come: + +```python +import argparse +import contextlib +import io +import sys +from time import sleep + +from game_of_life import parse_board, board_to_df, step +import polars as pl + + +class Application: + + # Initialize the board + def __init__(self): ... + + # Printing the application object prints the board + def __str__(self) -> str: ... + + # Run a step of the simulation every `delay` steps, for `n` maximum steps + def start(self, n, delay, print_df): ... +``` + +Notice how we're importing `parse_board`, `board_to_df` and `step` from our fully-developed plugin. +This could've been installed with pip! Check the [publishing chapter](publishing.md) for more on this. + +So first things first: `__init__`. +Here we use the stdlib `argparse` module to capture the command line arguments we mentioned above. +Then, we call `board_to_df` with the result of `parse_board`, storing the resulting DataFrame in the `Application` object itself. + +```python +class Application: + + def __init__(self): + self._args = argparse.Namespace() + cli = argparse.ArgumentParser( + prog="python -m game_of_life", description="Options" + ) + cli.add_argument("-i", "--input", type=str, required=True) + cli.add_argument("-d", "--delay", type=float, default=0.2) + cli.add_argument("-n", "--num-steps", type=int, default=sys.maxsize) + + cli.parse_args(namespace=self._args) + + # [-i] + self.ifile: str = self._args.input + + # [-d] + self.delay: float = self._args.delay + + # [-n] + self.steps: int = self._args.num_steps + + # Creates a pl.DataFrame from the provided file + self.df = board_to_df(parse_board(self.ifile)) +``` + +Next, an optional but handy detail - we implement `__str__` for `Application` in a way that printing an `Application` object will actually print the DataFrame stored internally: + +```python +class Application: + + # ... + + def __str__(self) -> str: + res = io.StringIO() + with ( + pl.Config(tbl_rows=-1, tbl_cols=-1), + contextlib.redirect_stdout(res), + ): + print(self.df) + return res.getvalue() +``` + +The `pl.Config` part just removes the default row and column limits when displaying a DataFrame - otherwise we'd see ellipses (`...`) instead of `1`s and `0`s. + +Finally, `start` is where we display the DataFrame and call `step` to advance the simulation, over and over: + +```python +class Application: + + # ... + + def start( + self, + n: int | None = None, + delay: float | None = None, + print_df: bool = True, + ): + if n is None: + n = self.steps + + if delay is None: + delay = self.delay + + if print_df: + print(self) + + iteration_cnt = 0 + try: + for _ in range(n): + self.df = step(self.df) + iteration_cnt += 1 + if print_df: + # Clear screen + print("\033[2J") + print(self) + sleep(delay) + + except KeyboardInterrupt: + print( + f"\nKeyboard Interrupt: ran for {iteration_cnt} iterations. Aborting..." + ) + print(f"max_num_steps={self._args.num_steps}\ndelay={self._args.delay}") +``` \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 05271b3..b6c7085 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -29,6 +29,9 @@ nav: - publishing.md - aggregate.md - where_to_go.md + - ??. Can we run Doom?: + - life_pt1.md + - life_pt2.md plugins: - search From 4926a0ee7003ff14a4be3b32e98c03348e3d6607 Mon Sep 17 00:00:00 2001 From: Bruno Conde Kind Date: Fri, 2 Aug 2024 13:32:38 -0300 Subject: [PATCH 2/9] Fixed space(s) at the end of each file --- docs/life_pt1.md | 22 ---------------------- docs/life_pt2.md | 2 +- 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/docs/life_pt1.md b/docs/life_pt1.md index 23c1804..1b67748 100644 --- a/docs/life_pt1.md +++ b/docs/life_pt1.md @@ -291,25 +291,3 @@ fn life_step(inputs: &[Series]) -> PolarsResult { Awesome, now what? If we ignore tests, _as plugin developers_, we could say we're done. Nothing's happened yet, so how could we be done? In the next section we'll take a look at how the plugin _user_ would call the functions we made available. - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/life_pt2.md b/docs/life_pt2.md index 5f4b3d1..2ffe705 100644 --- a/docs/life_pt2.md +++ b/docs/life_pt2.md @@ -123,4 +123,4 @@ class Application: f"\nKeyboard Interrupt: ran for {iteration_cnt} iterations. Aborting..." ) print(f"max_num_steps={self._args.num_steps}\ndelay={self._args.delay}") -``` \ No newline at end of file +``` From 71a555d379f1f43d7eafae527cff93132e029c85 Mon Sep 17 00:00:00 2001 From: Bruno Conde Kind Date: Wed, 7 Aug 2024 12:06:38 -0300 Subject: [PATCH 3/9] Added gif showing the result of the plugin --- docs/assets/life_toad_df.gif | Bin 0 -> 20033 bytes docs/life_pt1.md | 4 ++-- docs/life_pt2.md | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 docs/assets/life_toad_df.gif diff --git a/docs/assets/life_toad_df.gif b/docs/assets/life_toad_df.gif new file mode 100644 index 0000000000000000000000000000000000000000..974452d6255236976576d2b9b579a908f0a46d1d GIT binary patch literal 20033 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 literal 0 HcmV?d00001 diff --git a/docs/life_pt1.md b/docs/life_pt1.md index 1b67748..1a88666 100644 --- a/docs/life_pt1.md +++ b/docs/life_pt1.md @@ -253,8 +253,8 @@ fn life_step(inputs: &[Series]) -> PolarsResult { let len = lf.len(); - let mut out: Int64Chunked = ca_curr - .into_no_null_iter() + let mut out: Int64Chunked = mid + .iter() .enumerate() .map(|(idx, val)| { // Neighbours above diff --git a/docs/life_pt2.md b/docs/life_pt2.md index 2ffe705..dc39803 100644 --- a/docs/life_pt2.md +++ b/docs/life_pt2.md @@ -124,3 +124,35 @@ class Application: ) print(f"max_num_steps={self._args.num_steps}\ndelay={self._args.delay}") ``` + +To run the program, we only need two more things - an entry point and an input file. +Create a `toad.txt` in an `input` folder, containing: + +``` +00000000000 +00000000000 +00000000000 +00001110000 +00011100000 +00000000000 +00000000000 +00000000000 +``` + +and add this entry point to the end of `run.py`: + +```python +if __name__ == "__main__": + app = Application() + app.start() +``` + +Now we can see the results of our work, at last: + +```shell +python run.py -i input/toad.txt -d 0.3 +``` + +![Toad pattern with period = 2, running in a dataframe](assets/life_toad_df.gif) + +__Victory!__ \ No newline at end of file From 42147fa1f26adee849eda2f2f6b32bc08605dfe2 Mon Sep 17 00:00:00 2001 From: Bruno Conde Kind Date: Wed, 7 Aug 2024 12:15:01 -0300 Subject: [PATCH 4/9] Added reference + plugin code to expressions.rs and __init__.py --- docs/life_pt2.md | 6 ++++- minimal_plugin/__init__.py | 8 ++++++ src/expressions.rs | 50 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/docs/life_pt2.md b/docs/life_pt2.md index dc39803..3d135ae 100644 --- a/docs/life_pt2.md +++ b/docs/life_pt2.md @@ -155,4 +155,8 @@ python run.py -i input/toad.txt -d 0.3 ![Toad pattern with period = 2, running in a dataframe](assets/life_toad_df.gif) -__Victory!__ \ No newline at end of file +__Victory!__ + +## Reference + +The entire code for this plugin, including the user's side can be found on [GitHub](https://github.com/condekind/life_polars_plugin) diff --git a/minimal_plugin/__init__.py b/minimal_plugin/__init__.py index 53c7d48..19a2597 100644 --- a/minimal_plugin/__init__.py +++ b/minimal_plugin/__init__.py @@ -140,3 +140,11 @@ def interpolate(expr: IntoExpr) -> pl.Expr: symbol="interpolate", is_elementwise=False, ) + +def life_step(left: IntoExpr, mid: IntoExpr, right: IntoExpr) -> pl.Expr: + return register_plugin( + args=[left, mid, right], + lib=lib, + symbol="life_step", + is_elementwise=False, + ) diff --git a/src/expressions.rs b/src/expressions.rs index 1a7b1c9..59977ae 100644 --- a/src/expressions.rs +++ b/src/expressions.rs @@ -432,3 +432,53 @@ fn interpolate(inputs: &[Series]) -> PolarsResult { out.rename(ca.name()); Ok(out.into_series()) } + +#[polars_expr(output_type=Int64)] +fn life_step(inputs: &[Series]) -> PolarsResult { + let (ca_lf, ca_curr, ca_rt) = (inputs[0].i64()?, inputs[1].i64()?, inputs[2].i64()?); + + let lf = ca_lf + .cont_slice() + .expect("Expected input to be contiguous (in a single chunk)"); + let mid = ca_curr + .cont_slice() + .expect("Expected input to be contiguous (in a single chunk)"); + let rt = ca_rt + .cont_slice() + .expect("Expected input to be contiguous (in a single chunk)"); + + let len = lf.len(); + + let mut out: Int64Chunked = mid + .iter() + .enumerate() + .map(|(idx, val)| { + // Neighbours above + let prev_row = if 0 == idx { + lf[len - 1] + mid[len - 1] + rt[len - 1] + } else { + lf[idx - 1] + mid[idx - 1] + rt[idx - 1] + }; + + // Curr row does not include cell in the middle, + // a cell is not a neighbour of itself + let curr_row = lf[idx] + rt[idx]; + + // Neighbours below + let next_row = if len - 1 == idx { + lf[0] + mid[0] + rt[0] + } else { + lf[idx + 1] + mid[idx + 1] + rt[idx + 1] + }; + + // Life logic + Some(match (val, prev_row + curr_row + next_row) { + (1, 2) | (1, 3) => 1, + (0, 3) => 1, + _ => 0, + }) + }) + .collect_trusted(); + out.rename(ca_curr.name()); + Ok(out.into_series()) +} From 25e7d877963a2c57dfef10cdcfe3e10b8fb1405f Mon Sep 17 00:00:00 2001 From: Bruno Conde Kind Date: Wed, 7 Aug 2024 12:35:52 -0300 Subject: [PATCH 5/9] Added numbers to chapters, shifted following chapters --- docs/aggregate.md | 2 +- docs/life_pt1.md | 2 +- docs/life_pt2.md | 2 +- docs/publishing.md | 2 +- mkdocs.yml | 6 +++--- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/aggregate.md b/docs/aggregate.md index fdf8a9f..96da43e 100644 --- a/docs/aggregate.md +++ b/docs/aggregate.md @@ -1,4 +1,4 @@ -# 14. In (the) aggregate +# 15. In (the) aggregate Enough transorming columns! Let's aggregate them instead. diff --git a/docs/life_pt1.md b/docs/life_pt1.md index 1a88666..4f5a647 100644 --- a/docs/life_pt1.md +++ b/docs/life_pt1.md @@ -1,5 +1,5 @@ -# ??.1 Well... +# 13.1 Well... > "No." - _Doom Slayer_ diff --git a/docs/life_pt2.md b/docs/life_pt2.md index 3d135ae..71f61b3 100644 --- a/docs/life_pt2.md +++ b/docs/life_pt2.md @@ -1,5 +1,5 @@ -# ??.2 Plugin user +# 13.2 Plugin user In the last section we saw what the plugin developers made available for a plugin user. Now we put the user's hat and demonstrate that _usage_. diff --git a/docs/publishing.md b/docs/publishing.md index 9fd26a6..3f50295 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -1,4 +1,4 @@ -# 13. Publishing your plugin to PyPI and becoming famous +# 14. Publishing your plugin to PyPI and becoming famous Here are the steps you should follow: diff --git a/mkdocs.yml b/mkdocs.yml index b6c7085..665562d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -26,12 +26,12 @@ nav: - struct.md - lost_in_space.md - vec_of_option.md + - 13. Can we run Doom?: + - life_pt1.md + - life_pt2.md - publishing.md - aggregate.md - where_to_go.md - - ??. Can we run Doom?: - - life_pt1.md - - life_pt2.md plugins: - search From 40f15a36916afa323874370e0b551ebeec45402a Mon Sep 17 00:00:00 2001 From: Bruno Conde Kind Date: Mon, 12 Aug 2024 09:42:32 -0300 Subject: [PATCH 6/9] Typos and minor changes --- docs/life_pt1.md | 4 ++-- docs/life_pt2.md | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/life_pt1.md b/docs/life_pt1.md index 4f5a647..9de1a00 100644 --- a/docs/life_pt1.md +++ b/docs/life_pt1.md @@ -164,7 +164,7 @@ Do we stop the simulation from propagating there, do we wrap around, or somethin Many implementations stop the simulation at the border, so let's do it differently, let's wrap around! Wait, why are we talking about this here - isn't this a concern to be solved by our plugin in Rust? -Yes, but in Python-land is where we name our columns. +Yes, but Python-land is where we name our columns. So in order to have that nice overriding behavior, we need to address it here. This is also a hint at what the mysterious `_nwise_wrapping` function does: @@ -213,7 +213,7 @@ def step(df: pl.DataFrame, n: int = 1): ``` We're done with the Python side of things. -And if you're wondering: "what plugin did we actually register with `life_step`" - +And if you're wondering: "what plugin did we actually register with `life_step`?" - you're totally right to be confused, we didn't touch Rust yet! Why did we leave it for last? Because surprisingly, it's much simpler than the Python side, and much shorter too. diff --git a/docs/life_pt2.md b/docs/life_pt2.md index 71f61b3..4ee035b 100644 --- a/docs/life_pt2.md +++ b/docs/life_pt2.md @@ -5,7 +5,7 @@ In the last section we saw what the plugin developers made available for a plugi Now we put the user's hat and demonstrate that _usage_. For this, we'll implement a CLI app that will parse a board file provided as an argument, then run a step of the simulation every `delay` seconds (also provided as an argument). -> Tip: place the code in this section in a separate file, e.g., `run.py`. +> Tip: place the code from this section in a separate file, e.g., `run.py`. Just like what we did previously, let's look at an overview of what's to come: @@ -139,7 +139,7 @@ Create a `toad.txt` in an `input` folder, containing: 00000000000 ``` -and add this entry point to the end of `run.py`: +and add this entry point at the end of `run.py`: ```python if __name__ == "__main__": @@ -150,6 +150,10 @@ if __name__ == "__main__": Now we can see the results of our work, at last: ```shell +# Compile the rust code +maturin develop --release + +# Run the application python run.py -i input/toad.txt -d 0.3 ``` From e585d01abaea2626d79842d27a6637e3f3a1dd46 Mon Sep 17 00:00:00 2001 From: Bruno Conde Kind Date: Mon, 12 Aug 2024 10:40:20 -0300 Subject: [PATCH 7/9] Update docs/life_pt2.md Co-authored-by: Marco Edward Gorelli --- docs/life_pt2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/life_pt2.md b/docs/life_pt2.md index 4ee035b..26610f7 100644 --- a/docs/life_pt2.md +++ b/docs/life_pt2.md @@ -163,4 +163,4 @@ __Victory!__ ## Reference -The entire code for this plugin, including the user's side can be found on [GitHub](https://github.com/condekind/life_polars_plugin) +The entire code for this plugin, including the user's side can be found on [GitHub](https://github.com/condekind/life_polars_plugin). From 77296dc407fadf3aa953fac4b95aaa595fbc7422 Mon Sep 17 00:00:00 2001 From: Bruno Conde Kind Date: Mon, 12 Aug 2024 10:51:15 -0300 Subject: [PATCH 8/9] Suggestions from review --- docs/aggregate.md | 2 +- docs/life_pt1.md | 6 +++++- docs/life_pt2.md | 2 +- docs/publishing.md | 2 +- mkdocs.yml | 6 +++--- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/aggregate.md b/docs/aggregate.md index 96da43e..fdf8a9f 100644 --- a/docs/aggregate.md +++ b/docs/aggregate.md @@ -1,4 +1,4 @@ -# 15. In (the) aggregate +# 14. In (the) aggregate Enough transorming columns! Let's aggregate them instead. diff --git a/docs/life_pt1.md b/docs/life_pt1.md index 9de1a00..72a45d8 100644 --- a/docs/life_pt1.md +++ b/docs/life_pt1.md @@ -1,8 +1,12 @@ -# 13.1 Well... +# Extra.1 Well... > "No." - _Doom Slayer_ +
+ +> ⚠️ __Note:__ This section is completely optional, and is provided for a bit of nerdy fun. It is by no means essential, feel free to skip it if it doesn't interest you! + Well, someone can, probably. But doom in a dataframe would be kinda hard to play, so let's try something simpler. [Conway's Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) is a notorious Cellular Automaton that we could perhaps implement with a plugin. For science, of course. diff --git a/docs/life_pt2.md b/docs/life_pt2.md index 26610f7..9a863f8 100644 --- a/docs/life_pt2.md +++ b/docs/life_pt2.md @@ -1,5 +1,5 @@ -# 13.2 Plugin user +# Extra.2 Plugin user In the last section we saw what the plugin developers made available for a plugin user. Now we put the user's hat and demonstrate that _usage_. diff --git a/docs/publishing.md b/docs/publishing.md index 3f50295..9fd26a6 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -1,4 +1,4 @@ -# 14. Publishing your plugin to PyPI and becoming famous +# 13. Publishing your plugin to PyPI and becoming famous Here are the steps you should follow: diff --git a/mkdocs.yml b/mkdocs.yml index 665562d..440a3b5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -26,11 +26,11 @@ nav: - struct.md - lost_in_space.md - vec_of_option.md - - 13. Can we run Doom?: - - life_pt1.md - - life_pt2.md - publishing.md - aggregate.md + - "Extra: Can we run Doom?": + - life_pt1.md + - life_pt2.md - where_to_go.md plugins: From 3aa6170d818c58dd0e5b65c8c4c7fca5a3577150 Mon Sep 17 00:00:00 2001 From: Marco Gorelli <33491632+MarcoGorelli@users.noreply.github.com> Date: Mon, 12 Aug 2024 16:07:17 +0100 Subject: [PATCH 9/9] minor updates --- docs/life_pt1.md | 26 ++++++++++++++++++-------- docs/life_pt2.md | 10 +++++----- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/docs/life_pt1.md b/docs/life_pt1.md index 72a45d8..5b9de0a 100644 --- a/docs/life_pt1.md +++ b/docs/life_pt1.md @@ -5,7 +5,10 @@
-> ⚠️ __Note:__ This section is completely optional, and is provided for a bit of nerdy fun. It is by no means essential, feel free to skip it if it doesn't interest you! +!!!note + This section is completely optional, and is provided for a bit + of nerdy fun. It is by no means essential, feel free to skip + it if it doesn't interest you! Well, someone can, probably. But doom in a dataframe would be kinda hard to play, so let's try something simpler. [Conway's Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) is a notorious Cellular Automaton that we could perhaps implement with a plugin. @@ -16,7 +19,11 @@ For science, of course. Jokes aside, life allows us to show how a plugin can access elements in both neighbouring rows and columns for each element. With a little bit of extra Python, we can display things in an almost pretty manner. -> __Note:__ For this tutorial, we'll assume you created a new plugin from the cookiecutter template and named it `game_of_life` (these steps aren't shown here, since they were already covered at the very beginning of this series). +!!!note + For this tutorial, we'll assume you created a new plugin from the + cookiecutter template and named it `game_of_life` + (these steps aren't shown here, since they were already covered at the + very beginning of this series). In this section we'll cover the developer side of the plugin (both Python and Rust). In the next section we'll show how a user can import and use what we developed here. @@ -62,11 +69,13 @@ Starting with the function to parse a board from a file or stdin: ```python def parse_board( - ifile: str - | bytes - | PathLike[str] - | PathLike[bytes] - | Iterable[str | bytes | PathLike[str] | PathLike[bytes]], + ifile: ( + str + | bytes + | PathLike[str] + | PathLike[bytes] + | Iterable[str | bytes | PathLike[str] | PathLike[bytes]] + ), ) -> list[list[int]]: """ Converts a board in a file containing only 0s and 1s, e.g.:: @@ -141,7 +150,8 @@ def board_to_df(board: list[list[int]]) -> pl.DataFrame: Let's skip `_nwise_wrapping` and `step` for now and jump straight to the last function - we'll return to the two we skipped soon: -> Don't forget to read the comments! +!!!note + Don't forget to read the comments! ```python def life_step(left: IntoExpr, mid: IntoExpr, right: IntoExpr) -> pl.Expr: diff --git a/docs/life_pt2.md b/docs/life_pt2.md index 9a863f8..38c033b 100644 --- a/docs/life_pt2.md +++ b/docs/life_pt2.md @@ -93,10 +93,10 @@ class Application: # ... def start( - self, - n: int | None = None, - delay: float | None = None, - print_df: bool = True, + self, + n: int | None = None, + delay: float | None = None, + print_df: bool = True, ): if n is None: n = self.steps @@ -163,4 +163,4 @@ __Victory!__ ## Reference -The entire code for this plugin, including the user's side can be found on [GitHub](https://github.com/condekind/life_polars_plugin). +The entire code for this plugin, including the user's side, can be found on [GitHub](https://github.com/condekind/life_polars_plugin).