import json
import sys
from abc import ABC, abstractmethod
from subprocess import PIPE
from typing import Iterable
import numpy
from attrs import Factory, define
from monolithium import rustlith
sys.modules["anywidget"] = None
import altair
MILLION: int = 1_000_000
@define(eq=False)
class Monolith:
area: int
seed: int
minx: int
maxx: int
minz: int
maxz: int
def __hash__(self) -> int:
return hash((
self.seed,
self.minx, self.maxx,
self.minz, self.maxz,
))
class FitModel(ABC):
@abstractmethod
def __call__(self, *args: float) -> float:
...
@abstractmethod
def function(self, *scalars: float) -> str:
...
class Models:
class Exponential(FitModel):
def __call__(self, x: float, a: float, b: float) -> float:
return a*numpy.exp(b*x)
def function(self, *scalars: float) -> str:
a, b = scalars
return f"y = ({a:.2f}) e^({b:.8f}x)"
@define
class Distribution:
monoliths: list[Monolith] = Factory(list)
@property
def sorted_areas(self) -> Iterable[int]:
yield from sorted(mono.area for mono in self.monoliths)
def filter_unique(self) -> None:
self.monoliths = list(set(self.monoliths))
def smart_rustlith(self, *args: list[str]) -> None:
process = rustlith(*args, stdout=PIPE, Popen=True)
for line in process.stdout.readlines():
line = line.decode("utf-8").strip()
if not line.startswith("json"):
continue
line = line.removeprefix("json")
mono = Monolith(**json.loads(line))
self.monoliths.append(mono)
def multi(self) -> None:
self.smart_rustlith(
"spawn", "--radius", 200, "--step", 50,
"random", "--total", int(50e6),
"--fast",
)
def points() -> Iterable[tuple[float, float]]:
for i, area in enumerate(self.sorted_areas):
y = 100*(1 - i/len(self.monoliths))
x = area
yield (x, y)
self.simple_chart(
points=points(),
title="Monolith Area Distribution (multi world)",
xname="Area",
yname="Monoliths (%)",
fit=Models.Exponential(),
fit_scale_x=(1/MILLION),
fit_scale_y=(1/100),
).save(
fp="/tmp/multi.png",
scale_factor=3.0
)
def world(self,
seed: int=617,
step: int=512,
) -> None:
self.smart_rustlith("find",
"--step", step,
"--seed", seed,
)
self.filter_unique()
def points() -> Iterable[tuple[float, float]]:
for i, area in enumerate(self.sorted_areas):
y = len(self.monoliths) - i
x = area
yield (x, y)
self.simple_chart(
points=points(),
title=f"Monolith Area Distribution (seed={seed})",
xname="Area",
yname="Number of Monoliths",
fit=Models.Exponential(),
fit_scale_x=(1/MILLION),
).save(
fp="/tmp/world.png",
scale_factor=3.0
)
def simple_chart(self,
points: Iterable[tuple[float, float]]=None,
title: str="Untitled Chart",
xname: str="Untitled X Axis",
yname: str="Untitled Y Axis",
fit: FitModel=None,
fit_scale_x: float=1.0,
fit_scale_y: float=1.0,
) -> altair.Chart:
from scipy.optimize import curve_fit
points = list(points or [])
x = tuple(xi for xi, _ in points)
y = tuple(yi for _, yi in points)
if (fit is not None):
scalars, covariance = curve_fit(f=fit,
xdata=list(n*fit_scale_x for n in x),
ydata=list(n*fit_scale_y for n in y),
)
title = f"{title} • {fit.function(*scalars)}"
chart = altair.Chart(altair.Data(values=[
dict(x=xi, y=yi) for xi, yi in zip(x, y)
])).mark_line().encode(
x=altair.X("x:Q", title=xname),
y=altair.Y("y:Q", title=yname),
).properties(
title=title,
width=1920/2,
height=720/2,
)
return chart
def heatmap(self,
) -> altair.Chart:
self.smart_rustlith(
"spawn",
"--radius", 2**19,
"--step", 256,
"linear",
"--total", 1000,
)
chart = altair.Chart(altair.Data(values=[
dict(x=mono.minx, z=mono.minz, a=mono.area) for mono in self.monoliths
])).mark_circle().encode(
x=altair.X("x:Q", title="X"),
y=altair.Y("z:Q", title="Z"),
size=altair.Size("a:Q", title="Area", scale=altair.Scale(range=[1, 100]))
).properties(
title="Monolith Positions",
width=1920/2,
height=1080/2,
)
for v, c in ((2**18, "red"), (2**19, "green")):
chart += altair.Chart(altair.Data(values=[
dict(x= v, z= v),
dict(x= v, z=-v),
dict(x=-v, z=-v),
dict(x=-v, z= v),
dict(x= v, z= v),
dict(x= v, z=-v),
])).mark_line(color=c).encode(
x=altair.X("x:Q"),
y=altair.Y("z:Q"),
)
return chart
def main() -> None:
stats = Distribution()
stats.heatmap().save(
fp="/tmp/heatmap.png",
scale_factor=3.0
)