From 80c18f2933fea081c3fc701636e8c1dbb1919090 Mon Sep 17 00:00:00 2001 From: Evan Kiefl Date: Fri, 13 Sep 2024 21:58:38 -0700 Subject: [PATCH 1/3] Add a second example --- docs/examples/straight_shot.pct.py | 313 +++++++++++++++++++++++++++++ pooltool/ai/aim/core.py | 83 ++++++-- 2 files changed, 384 insertions(+), 12 deletions(-) create mode 100644 docs/examples/straight_shot.pct.py diff --git a/docs/examples/straight_shot.pct.py b/docs/examples/straight_shot.pct.py new file mode 100644 index 00000000..ab9f6bff --- /dev/null +++ b/docs/examples/straight_shot.pct.py @@ -0,0 +1,313 @@ +# --- +# jupyter: +# jupytext: +# notebook_metadata_filter: all +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.16.4 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# language_info: +# codemirror_mode: +# name: ipython +# version: 3 +# file_extension: .py +# mimetype: text/x-python +# name: python +# nbconvert_exporter: python +# pygments_lexer: ipython3 +# version: 3.12.4 +# --- + +# %% [markdown] editable=true slideshow={"slide_type": ""} +# # What straight shot is most difficult? +# +# Consider a straight shot into a pocket where the cue ball (CB) is a total distance $D$ from the pocket. Which object ball (OB) position (distance $d$ from the CB) results in the most difficult pot? +# +# This question is posed by Dr. Dave Billiards in [this proof](https://billiards.colostate.edu/technical_proofs/TP_3-4.pdf) and based on the assumptions made, the answer is +# +# $$ +# d = D/2 +# $$ +# +# In other words, the shot is made most difficult when the OB is placed equidistantly from both the pocket and the CB. +# +# In this example, we will directly simulate straight shots and determine under what circumstances this theory holds up. + +# %% [markdown] +# ## Setting up a system +# +# Let's set up a system where the OB is shot into a corner pocket _straight on_ (at an angle of 45 degrees from both the long cushion and short cushion). +# +# We'll begin by creating a big square [Table](../autoapi/pooltool/index.rst#pooltool.Table) that gives us plenty of space. There are many different parameters that can be passed to [PocketTableSpecs](../autoapi/pooltool/objects/index.rst#pooltool.objects.PocketTableSpecs) that influence the shape of the pockets (you can get a visual explanation of each parameter [here](../resources/table_specs.md)), but here we will keep the default values, which are modelled after my old 7-foot Showood table with pretty wide 5 inch pockets. + +# %% +import pooltool as pt + +table_specs = pt.objects.PocketTableSpecs(l=10, w=10) +table = pt.Table.from_table_specs(table_specs) + +# %% [markdown] +# As a reminder, $D$ represents the distance of the CB to the pocket, but let's be a little more precise with what that means. From the CB, we will measure from the ball's center, and from the pocket we will measure from pocket's edge that's closest to the CB. So when $D$ is 0, the CB is teetering on the edge of the pocket. +# +# With that in mind, let's pick the bottom-left corner [Pocket](../autoapi/pooltool/objects/index.rst#pooltool.objects.Pocket) as our target and place a CB a distance $D$ away. + +# %% +import numpy as np + +# lb stands for left-bottom +pocket = table.pockets["lb"] + +# The pocket's center is defined with 3D coordinates, but we'll just use X and Y +center = pocket.center[:2] + +pocket_point = center + pocket.radius * np.sqrt(2) + +print(f"The pocket point is {pocket_point}") + +# %% [markdown] +# Now let's create a cue ball a distance $D$ away. + +# %% +D = 1.0 + +def create_cue_ball(pocket_point, D): + cue_point = pocket_point + D * np.sqrt(2) + return pt.Ball.create("CB", xy=cue_point) + +cue_ball = create_cue_ball(pocket_point, D) + + +# %% [markdown] +# Let's create an object ball a distance $d$ from the cue ball. In order for the object ball to be between the cue and the pocket, $d$ is constrained by +# +# $$ +# 2 R \lt d \lt D +# $$ +# +# where $R$ is the radius of the balls. + +# %% +def create_object_ball(cue_ball, d): + obj_point = cue_ball.xyz[:2] - d * np.sqrt(2) + return pt.Ball.create("OB", xy=obj_point) + +d = 0.3 + +obj_ball = create_object_ball(cue_ball, d) + + +# %% [markdown] +# Let's go ahead and turn everything we've done into a function that takes $d$ and $D$ as parameters and returns a [System](../autoapi/pooltool/index.rst#pooltool.System). + +# %% +def create_system(d, D): + table_specs = pt.objects.PocketTableSpecs(l=10, w=10) + table = pt.Table.from_table_specs(table_specs) + pocket = table.pockets["lb"] + pocket_point = pocket.center[:2] + pocket.radius * np.sqrt(2) + + cue_ball = create_cue_ball(pocket_point, D) + obj_ball = create_object_ball(cue_ball, d) + + return pt.System( + cue=pt.Cue.default(), + balls=(cue_ball, obj_ball), + table=table, + ) + + +# %% [markdown] +# With this function, we can create a system: + +# %% +system = create_system(d, D) + +# %% [markdown] +# If you have a graphics card, you can visualize the system in 3D with +# +# ```python +# gui = pt.ShotViewer() +# gui.show(system) +# ``` +# +# ## Simulating a shot +# +# No energy has been imparted into the system so it's currently rather dull. Let's change that by striking the CB in the direction of the OB. + +# %% +# phi is the aiming direction +phi = pt.aim.at_ball(cue_ball=system.balls["CB"], object_ball=system.balls["OB"], cut=0) + +# Based on the geometry, we know that phi is 225 degrees (towards the bottom-left pocket) +assert phi == 225.0 + +system.cue.set_state(V0=1, phi=phi, cue_ball_id="CB") + +# %% [markdown] +# Since the pocket, OB, and CB are all lined up, this should lead to a successful pot. Let's check by simulating and filtering the system events according to our criteria. + +# %% +pt.simulate(system, inplace=True) + +def successful_pot(system) -> bool: + # Find events that are ball-pocket events and involve the object ball + pocket_events = pt.events.filter_events( + system.events, + pt.events.by_type(pt.EventType.BALL_POCKET), + pt.events.by_ball("OB"), + ) + return bool(len(pocket_events)) + +successful_pot(system) + + +# %% [markdown] +# Great! As expected, it went straight in. + +# %% [markdown] +# ## Modeling imprecision +# +# Real pool players don't have perfect precision and we are going to emulate this by introducing uncertainty into the aiming angle $\phi$. We'll call this uncertainty $d\phi$. +# +# While we're at it, let's make a function that adds uncertainty not just to $\phi$, but to the other cue striking parameters as well, that you can read about [here](../autoapi/pooltool/index.rst#pooltool.Cue). + +# %% +def perturb(x, dx): + return x + dx * (2*np.random.rand() - 1) + +def strike(system, V0, a, phi, theta, dV0=0, da=0, dphi=0, dtheta=0): + system.cue.set_state( + cue_ball_id="CB", + V0=perturb(V0, dV0), + a=perturb(a, da), + phi=perturb(phi, dphi), + theta=perturb(theta, dtheta), + ) + + +# %% [markdown] +# ## Estimating Difficulty for a Given $d$ +# +# To evaluate the difficulty level associated with shot where the OB is a distance $d$ from the CB, we will calculate the fraction of successful pots (successful shots) from a set of trials. In these trials, we will introduce random perturbations to the angle $\phi$ for each shot. The perturbations are limited so that the angle $\phi$ deviates by no more than a fixed amount $d\phi$. +# +# This means that for each shot, the angle $\phi$ is randomly adjusted within the range $[\phi - d\phi, \phi + d\phi]$. By observing how these perturbations affect the success rate, we can estimate the difficulty associated with the given $d$. +# +# Let's pick a $d\phi$ and then simulate 100 shots. + +# %% +dphi = 0.4 # Just 0.4 degrees! +phi = 225.0 +V0 = 1.0 # 1 m/s cue-stick strike +a = 0 # No sidespin +theta = 0 # Level cue stick +d = 0.3 +D = 1.0 + +num_trials = 100 +successes = [] +for _ in range(num_trials): + system = create_system(d, D) + strike(system, V0, a, phi, theta, dphi=dphi) + pt.simulate(system, inplace=True) + success = successful_pot(system) + successes.append(success) + +pot_fraction = sum(successes) / len(successes) +print(f"The success rate is {pot_fraction*100:.0f}%.") + +# %% [markdown] +# ## Which $d$ yields the most difficult shot? + +# %% [markdown] +# Great. Now, we can run the same experiment, but vary $d$ to determine which yields the most difficult shot. + +# %% +import numpy as np +import matplotlib.pyplot as plt + +# Vary from 1mm separation between CB and OB to 1mm from pocket's edge +d_values = np.linspace(2.001*cue_ball.params.R, D-0.001, 15) + +num_trials = 400 +pot_fractions = [] + +for d in d_values: + successes = [] + for _ in range(num_trials): + system = create_system(d, D) + strike(system, V0, a, phi, theta, dphi=dphi) + pt.simulate(system, inplace=True) + success = successful_pot(system) + successes.append(success) + + pot_fraction = sum(successes) / len(successes) + pot_fractions.append(pot_fraction) + print(f"For d = {d:.2f} m, the success rate is {pot_fraction*100:.0f}%.") + +plt.figure(figsize=(8, 6)) +plt.plot(d_values, pot_fractions, marker='o') +plt.xlabel('Distance d (m)') +plt.ylabel('Success Rate') +plt.title('Success Rate vs. Distance d') +plt.grid(True) +plt.show() + +# %% [markdown] editable=true slideshow={"slide_type": ""} +# Reading left to right, shot success starts high because the OB is close to the CB. But as the OB moves further away from the CB, the success rate decreases, reaching a minimum at around $D/2$. Past this point, the success rate starts to go up again, since the OB is now getting closer and closer to the pocket. +# +# Since this data is generated from a small number of trials, there is some variance in the plot. But if you increase `num_trials` to say, 5000, it's much more smooth. + +# %% [markdown] +# ## What about skill level? +# +# Since $d\phi$ is a measure of how precise the shot is, it is a reasonable proxy for skill level. With that in mind, how should we expect the shot success trajectory to vary as a function of skill level? Let's repeat the experiment for 3 different skill levels. + +# %% editable=true slideshow={"slide_type": ""} tags=["nbsphinx-gallery"] +# Define the range of 'd' values you want to test +d_values = np.linspace(2.001 * cue_ball.params.R, D - 0.001, 15) + +# Define the range of 'dphi' values (in degrees) +dphi_values = [0.15, 0.25, 0.5] + +num_trials = 300 +results = {} + +for dphi in dphi_values: + pot_fractions = [] + for d in d_values: + successes = [] + for _ in range(num_trials): + system = create_system(d, D) + strike(system, V0, a, phi, theta, dphi=dphi) + pt.simulate(system, inplace=True) + success = successful_pot(system) + successes.append(success) + + pot_fraction = sum(successes) / len(successes) + pot_fractions.append(pot_fraction) + + print(f"Finished {dphi=}") + results[dphi] = pot_fractions + +plt.figure(figsize=(8, 6)) +for dphi in dphi_values: + plt.plot(d_values, results[dphi], marker='o', label=f'dphi = {dphi}°') + +plt.xlabel('Distance d (m)') +plt.ylabel('Success Rate') +plt.title('Success Rate vs. Distance d for Different Skill (dphi) Values') +plt.legend() +plt.grid(True) +plt.show() + +# %% [markdown] +# Regardless of skill level, when $ \lim_{d \rightarrow 2R^{+}} $ and $ \lim_{d \rightarrow D^{-}} $, the success rate converges to $100\%$. That's expected—when the object ball OB is practically touching the CB or hanging over the pocket, potting becomes nearly automatic for all skill levels. +# +# But what’s more interesting is observing the extent and sharpness with which success rate drops when the OB is at an intermediate distance $d$. The least skilled players experience the earliest, sharpest, and lowest plummet in sucess rate, whereas high skilled players maintain higher success rate for longer. Yet no matter the skill, the minimum seems to be right at around $d=D/2$. + +# %% diff --git a/pooltool/ai/aim/core.py b/pooltool/ai/aim/core.py index 38095902..eff32603 100644 --- a/pooltool/ai/aim/core.py +++ b/pooltool/ai/aim/core.py @@ -43,22 +43,81 @@ def at_ball(system: System, ball_id: str, *, cut: float = 0.0) -> float: ... def at_ball(cue_ball: Ball, object_ball: Ball, *, cut: float = 0.0) -> float: ... -def at_ball(*args, **kwargs) -> float: # type: ignore +def at_ball(*args, **kwargs) -> float: """Returns phi to hit ball with specified cut angle (assumes straight line shot)""" - if len(kwargs): - assert len(kwargs) == 1 - assert "cut" in kwargs - if isinstance(system := args[0], System) and isinstance(args[1], str): - assert len(args) == 2 + # Extract 'cut' from kwargs, defaulting to 0.0 + cut = kwargs.pop("cut", 0.0) + + # Initialize variables + cue_ball = None + object_ball = None + system = None + ball_id = None + + # Collect positional arguments + positional_args = list(args) + + # Process keyword arguments + cue_ball_kwarg = kwargs.pop("cue_ball", None) + object_ball_kwarg = kwargs.pop("object_ball", None) + system_kwarg = kwargs.pop("system", None) + ball_id_kwarg = kwargs.pop("ball_id", None) + + # Assign positional arguments based on their types + while positional_args: + arg = positional_args.pop(0) + if isinstance(arg, System): + if system is not None: + raise TypeError("Multiple 'system' arguments provided") + system = arg + elif isinstance(arg, Ball): + if cue_ball is None: + cue_ball = arg + elif object_ball is None: + object_ball = arg + else: + raise TypeError("Too many Ball instances provided") + elif isinstance(arg, str): + if ball_id is not None: + raise TypeError("Multiple 'ball_id' arguments provided") + ball_id = arg + else: + raise TypeError( + f"Unexpected positional argument of type {type(arg).__name__}" + ) + + # Override with keyword arguments if provided + cue_ball = cue_ball_kwarg if cue_ball_kwarg is not None else cue_ball + object_ball = object_ball_kwarg if object_ball_kwarg is not None else object_ball + system = system_kwarg if system_kwarg is not None else system + ball_id = ball_id_kwarg if ball_id_kwarg is not None else ball_id + + # Validate combinations + if system and ball_id: + if cue_ball or object_ball: + raise TypeError( + "Provide either 'system' and 'ball_id', or 'cue_ball' and 'object_ball', not both" + ) cue_ball = system.balls[system.cue.cue_ball_id] - object_ball = system.balls[args[1]] - return _at_ball(cue_ball, object_ball, **kwargs) - elif isinstance(args[0], Ball) and isinstance(args[1], Ball): - assert len(args) == 2 - return _at_pos(*args, **kwargs) + object_ball = system.balls[ball_id] + elif cue_ball and object_ball: + pass # Both are already set else: - raise TypeError("Invalid arguments for at_ball") + raise TypeError( + "Invalid arguments: must provide 'cue_ball' and 'object_ball', or 'system' and 'ball_id'" + ) + + # Ensure no unexpected keyword arguments are left + if kwargs: + unexpected_args = ", ".join(kwargs.keys()) + raise TypeError(f"Unexpected keyword arguments: {unexpected_args}") + + # Validate types + if not isinstance(cue_ball, Ball) or not isinstance(object_ball, Ball): + raise TypeError("cue_ball and object_ball must be Ball instances") + + return _at_ball(cue_ball, object_ball, cut=cut) def _at_ball(cue_ball: Ball, object_ball: Ball, cut: float = 0.0) -> float: From 5819ae7c4c44ab2b6b176ce1dfce400061191363 Mon Sep 17 00:00:00 2001 From: Evan Kiefl Date: Fri, 13 Sep 2024 22:31:45 -0700 Subject: [PATCH 2/3] Finish straight shot example - Add docs to Makefile --- Makefile | 31 +++++++++++++++++++------- docs/Makefile | 26 ++++++++++++++-------- docs/README.md | 4 ++-- docs/examples/index.md | 1 + docs/examples/straight_shot.pct.py | 12 +++++----- docs/local.sh | 4 ---- docs/make.bat | 35 ------------------------------ docs/publish_instructions.md | 6 ++--- 8 files changed, 52 insertions(+), 67 deletions(-) delete mode 100644 docs/local.sh delete mode 100644 docs/make.bat diff --git a/Makefile b/Makefile index 83613408..92a06c2d 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,32 @@ -include .env +# Load environment variables from the `.env` file if it exists. +ifneq (,$(wildcard .env)) + include .env +endif + +.PHONY: docs +docs: + $(MAKE) -C docs/ clean-and-build-html + $(MAKE) -C docs/ view-html + +.PHONY: clean +clean: + rm -rf dist + +.PHONY: build +build: clean + poetry build # Note: `poetry` does not appear to read the `POETRY_PYPI_TOKEN_` environment variable, -# so we need to pass it explicitly in the `publish` command. -.PHONY: test-publish -test-publish: - echo ${POETRY_PYPI_TOKEN_PYPI_TEST} +# so we need to pass it explicitly in these publishing commands. +.PHONY: build-and-test-publish +build-and-test-publish: build poetry publish \ - --repository pypi-test \ + --repository pypi_test \ --username __token__ \ --password ${POETRY_PYPI_TOKEN_PYPI_TEST} -.PHONY: publish -publish: build +.PHONY: build-and-publish +build-and-publish: build poetry publish \ --username __token__ \ --password ${POETRY_PYPI_TOKEN_PYPI} diff --git a/docs/Makefile b/docs/Makefile index d4bb2cbb..4602635d 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,18 +1,26 @@ -# Minimal makefile for Sphinx documentation -# - # You can set these variables from the command line, and also # from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = . -BUILDDIR = _build +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build -# Put it first so that "make" without argument is like "make help". +# Put `help` first so that calling `make` without argument is like `make help`. +.PHONY: help help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -.PHONY: help Makefile +.PHONY: clean +clean: + rm -rf $(BUILDDIR) + +.PHONY: clean-and-build-html +clean-and-build-html: clean + @$(SPHINXBUILD) -M html "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: view-html +view-html: + open $(BUILDDIR)/html/index.html # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). diff --git a/docs/README.md b/docs/README.md index a9dfdebe..9e1f9907 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,8 +8,8 @@ poetry install --with docs Additionally, `pandoc` needs to be installed: https://pandoc.org/installing.html -Then navigate to this directory and run: +Then, in the root directory run: ``` -bash local.sh +make docs ``` diff --git a/docs/examples/index.md b/docs/examples/index.md index 022fb097..f39af960 100644 --- a/docs/examples/index.md +++ b/docs/examples/index.md @@ -3,5 +3,6 @@ ```{eval-rst} .. nbgallery:: 30_degree_rule + straight_shot ``` diff --git a/docs/examples/straight_shot.pct.py b/docs/examples/straight_shot.pct.py index ab9f6bff..0ebb518a 100644 --- a/docs/examples/straight_shot.pct.py +++ b/docs/examples/straight_shot.pct.py @@ -24,7 +24,7 @@ # --- # %% [markdown] editable=true slideshow={"slide_type": ""} -# # What straight shot is most difficult? +# # Straight shot difficulty # # Consider a straight shot into a pocket where the cue ball (CB) is a total distance $D$ from the pocket. Which object ball (OB) position (distance $d$ from the CB) results in the most difficult pot? # @@ -231,9 +231,9 @@ def strike(system, V0, a, phi, theta, dV0=0, da=0, dphi=0, dtheta=0): import matplotlib.pyplot as plt # Vary from 1mm separation between CB and OB to 1mm from pocket's edge -d_values = np.linspace(2.001*cue_ball.params.R, D-0.001, 15) +d_values = np.linspace(2.001*cue_ball.params.R, D-0.001, 12) -num_trials = 400 +num_trials = 200 pot_fractions = [] for d in d_values: @@ -269,12 +269,12 @@ def strike(system, V0, a, phi, theta, dV0=0, da=0, dphi=0, dtheta=0): # %% editable=true slideshow={"slide_type": ""} tags=["nbsphinx-gallery"] # Define the range of 'd' values you want to test -d_values = np.linspace(2.001 * cue_ball.params.R, D - 0.001, 15) +d_values = np.linspace(2.001 * cue_ball.params.R, D - 0.001, 12) # Define the range of 'dphi' values (in degrees) dphi_values = [0.15, 0.25, 0.5] -num_trials = 300 +num_trials = 200 results = {} for dphi in dphi_values: @@ -306,7 +306,7 @@ def strike(system, V0, a, phi, theta, dV0=0, da=0, dphi=0, dtheta=0): plt.show() # %% [markdown] -# Regardless of skill level, when $ \lim_{d \rightarrow 2R^{+}} $ and $ \lim_{d \rightarrow D^{-}} $, the success rate converges to $100\%$. That's expected—when the object ball OB is practically touching the CB or hanging over the pocket, potting becomes nearly automatic for all skill levels. +# Regardless of skill level, when $\lim_{d \rightarrow 2R^{+}}$ and $\lim_{d \rightarrow D^{-}}$, the success rate converges to $100\%$. That's expected—when the object ball OB is practically touching the CB or hanging over the pocket, potting becomes nearly automatic for all skill levels. # # But what’s more interesting is observing the extent and sharpness with which success rate drops when the OB is at an intermediate distance $d$. The least skilled players experience the earliest, sharpest, and lowest plummet in sucess rate, whereas high skilled players maintain higher success rate for longer. Yet no matter the skill, the minimum seems to be right at around $d=D/2$. diff --git a/docs/local.sh b/docs/local.sh deleted file mode 100644 index d80a8f57..00000000 --- a/docs/local.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -rm -rf autoapi _build -sphinx-build -b html . _build -open _build/index.html diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 153be5e2..00000000 --- a/docs/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -set BUILDDIR=_build - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.https://www.sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd diff --git a/docs/publish_instructions.md b/docs/publish_instructions.md index dfd57003..26a3f894 100644 --- a/docs/publish_instructions.md +++ b/docs/publish_instructions.md @@ -26,7 +26,7 @@ git push origin :refs/tags/ # If you pushed it ## 3. Build the distribution ```bash -poetry build +make build ``` You should see something like this: @@ -49,7 +49,7 @@ Also open `pooltool/__init__.py` and make sure the `__version__` variable was po - Populate your `.env` using `.env.copy` as a template. -- Run `make test-publish`. If this fails due to timing out (slow upload speed that gets cut short), skip ahead to trying out the installation locally. +- Run `make build-and-test-publish`. If this fails due to timing out (slow upload speed that gets cut short), run `make build` and then skip ahead to trying out the installation locally. - Create a fresh python environment to test the installation @@ -80,7 +80,7 @@ pip install dist/pooltool_billiards-${RELEASE_VERSION}.tar.gz --force-reinstall - Go back to the dev environment. -- Run `make publish` +- Run `make build-and-publish` - Create a new python environment From d8101a74f9b8e9adf61b835be5ed1e3c0d50aac1 Mon Sep 17 00:00:00 2001 From: Evan Kiefl Date: Fri, 13 Sep 2024 22:44:25 -0700 Subject: [PATCH 3/3] Fix title --- docs/examples/straight_shot.pct.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/straight_shot.pct.py b/docs/examples/straight_shot.pct.py index 0ebb518a..f91d0a5e 100644 --- a/docs/examples/straight_shot.pct.py +++ b/docs/examples/straight_shot.pct.py @@ -24,7 +24,7 @@ # --- # %% [markdown] editable=true slideshow={"slide_type": ""} -# # Straight shot difficulty +# # Straight Shot Difficulty # # Consider a straight shot into a pocket where the cue ball (CB) is a total distance $D$ from the pocket. Which object ball (OB) position (distance $d$ from the CB) results in the most difficult pot? #