state machine
This commit is contained in:
257
.gitignore
vendored
Normal file
257
.gitignore
vendored
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
### JetBrains template
|
||||||
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||||
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
|
|
||||||
|
# User-specific stuff
|
||||||
|
.idea/**/workspace.xml
|
||||||
|
.idea/**/tasks.xml
|
||||||
|
.idea/**/usage.statistics.xml
|
||||||
|
.idea/**/dictionaries
|
||||||
|
.idea/**/shelf
|
||||||
|
|
||||||
|
# AWS User-specific
|
||||||
|
.idea/**/aws.xml
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.idea/**/contentModel.xml
|
||||||
|
|
||||||
|
# Sensitive or high-churn files
|
||||||
|
.idea/**/dataSources/
|
||||||
|
.idea/**/dataSources.ids
|
||||||
|
.idea/**/dataSources.local.xml
|
||||||
|
.idea/**/sqlDataSources.xml
|
||||||
|
.idea/**/dynamic.xml
|
||||||
|
.idea/**/uiDesigner.xml
|
||||||
|
.idea/**/dbnavigator.xml
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
.idea/**/gradle.xml
|
||||||
|
.idea/**/libraries
|
||||||
|
|
||||||
|
# Gradle and Maven with auto-import
|
||||||
|
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||||
|
# since they will be recreated, and may cause churn. Uncomment if using
|
||||||
|
# auto-import.
|
||||||
|
# .idea/artifacts
|
||||||
|
# .idea/compiler.xml
|
||||||
|
# .idea/jarRepositories.xml
|
||||||
|
# .idea/modules.xml
|
||||||
|
# .idea/*.iml
|
||||||
|
# .idea/modules
|
||||||
|
# *.iml
|
||||||
|
# *.ipr
|
||||||
|
|
||||||
|
# CMake
|
||||||
|
cmake-build-*/
|
||||||
|
|
||||||
|
# Mongo Explorer plugin
|
||||||
|
.idea/**/mongoSettings.xml
|
||||||
|
|
||||||
|
# File-based project format
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
out/
|
||||||
|
|
||||||
|
# mpeltonen/sbt-idea plugin
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Cursive Clojure plugin
|
||||||
|
.idea/replstate.xml
|
||||||
|
|
||||||
|
# SonarLint plugin
|
||||||
|
.idea/sonarlint/
|
||||||
|
|
||||||
|
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||||
|
com_crashlytics_export_strings.xml
|
||||||
|
crashlytics.properties
|
||||||
|
crashlytics-build.properties
|
||||||
|
fabric.properties
|
||||||
|
|
||||||
|
# Editor-based Rest Client
|
||||||
|
.idea/httpRequests
|
||||||
|
|
||||||
|
# Android studio 3.1+ serialized cache file
|
||||||
|
.idea/caches/build_file_checksums.ser
|
||||||
|
|
||||||
|
### VirtualEnv template
|
||||||
|
# Virtualenv
|
||||||
|
# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
|
||||||
|
.Python
|
||||||
|
[Bb]in
|
||||||
|
[Ii]nclude
|
||||||
|
[Ll]ib
|
||||||
|
[Ll]ib64
|
||||||
|
[Ll]ocal
|
||||||
|
[Ss]cripts
|
||||||
|
pyvenv.cfg
|
||||||
|
.venv
|
||||||
|
pip-selfcheck.json
|
||||||
|
|
||||||
|
### Python template
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||||
|
.pdm.toml
|
||||||
|
.pdm-python
|
||||||
|
.pdm-build/
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
3
.idea/.gitignore
generated
vendored
Normal file
3
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
52
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
52
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="HtmlUnknownTag" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="myValues">
|
||||||
|
<value>
|
||||||
|
<list size="12">
|
||||||
|
<item index="0" class="java.lang.String" itemvalue="nobr" />
|
||||||
|
<item index="1" class="java.lang.String" itemvalue="noembed" />
|
||||||
|
<item index="2" class="java.lang.String" itemvalue="comment" />
|
||||||
|
<item index="3" class="java.lang.String" itemvalue="noscript" />
|
||||||
|
<item index="4" class="java.lang.String" itemvalue="embed" />
|
||||||
|
<item index="5" class="java.lang.String" itemvalue="script" />
|
||||||
|
<item index="6" class="java.lang.String" itemvalue="my-paragraph" />
|
||||||
|
<item index="7" class="java.lang.String" itemvalue="mc-diorama" />
|
||||||
|
<item index="8" class="java.lang.String" itemvalue="mc-world" />
|
||||||
|
<item index="9" class="java.lang.String" itemvalue="mc-frame" />
|
||||||
|
<item index="10" class="java.lang.String" itemvalue="mc-timeline" />
|
||||||
|
<item index="11" class="java.lang.String" itemvalue="mc-camera" />
|
||||||
|
</list>
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
<option name="myCustomValuesEnabled" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredPackages">
|
||||||
|
<list>
|
||||||
|
<option value="numpy" />
|
||||||
|
<option value="opencv-contrib-python-headless" />
|
||||||
|
<option value="joblib" />
|
||||||
|
<option value="loguru" />
|
||||||
|
<option value="matplotlib" />
|
||||||
|
<option value="pymatreader" />
|
||||||
|
<option value="tqdm" />
|
||||||
|
<option value="ipykernel" />
|
||||||
|
<option value="itk-montage" />
|
||||||
|
<option value="simpleitk" />
|
||||||
|
<option value="multiview-stitcher" />
|
||||||
|
<option value="itk-elastix" />
|
||||||
|
<option value="paraview" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PyStubPackagesAdvertiser" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredPackages">
|
||||||
|
<list>
|
||||||
|
<option value="pandas" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/wireless-formalism.iml" filepath="$PROJECT_DIR$/.idea/wireless-formalism.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
10
.idea/wireless-formalism.iml
generated
Normal file
10
.idea/wireless-formalism.iml
generated
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module external.system.id="pyproject.toml" type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="jdk" jdkName="~/src/wireless-formalism/.venv" jdkType="Python SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
509
finite_state.py
Normal file
509
finite_state.py
Normal file
@@ -0,0 +1,509 @@
|
|||||||
|
import math
|
||||||
|
from contextlib import redirect_stdout
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from itertools import islice
|
||||||
|
from typing import Tuple, Optional, List, Set
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Entry:
|
||||||
|
priority: int
|
||||||
|
delay: int
|
||||||
|
name: str = '-'
|
||||||
|
|
||||||
|
def linear_parts(self) -> Tuple[int, ...]:
|
||||||
|
if self.delay == 0:
|
||||||
|
return ()
|
||||||
|
return (self.priority,) + (-9999,) * (self.delay - 1)
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Transition:
|
||||||
|
from_state: str
|
||||||
|
to_state: str
|
||||||
|
entries: Tuple[Entry, ...]
|
||||||
|
|
||||||
|
def linearize(self) -> Tuple[int, ...]:
|
||||||
|
return tuple(p for e in self.entries for p in e.linear_parts())
|
||||||
|
|
||||||
|
def total_delay(self) -> int:
|
||||||
|
return sum(e.delay for e in self.entries)
|
||||||
|
|
||||||
|
class TilesetFSM:
|
||||||
|
def __init__(self, transitions: List[Transition], start_state: str, accept_states: Set[str]):
|
||||||
|
self.transitions = transitions
|
||||||
|
self.start_state = start_state
|
||||||
|
self.accept_states = accept_states
|
||||||
|
|
||||||
|
# Pre-group transitions by their source state for O(1) branching lookup
|
||||||
|
self.adj_list = {state: [] for state in set(t.from_state for t in transitions)}
|
||||||
|
for t in transitions:
|
||||||
|
self.adj_list[t.from_state].append(t)
|
||||||
|
|
||||||
|
def _check_divergence(self, prefix: Tuple[int, ...], target: Tuple[int, ...]) -> int:
|
||||||
|
"""
|
||||||
|
Returns -1 if prefix is lexicographically < target,
|
||||||
|
1 if prefix is lexicographically > target,
|
||||||
|
0 if prefix is a strict prefix of target (no divergence yet).
|
||||||
|
"""
|
||||||
|
for p_val, t_val in zip(prefix, target):
|
||||||
|
if p_val < t_val: return -1
|
||||||
|
if p_val > t_val: return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def find_next(self, current_lin: Tuple[int, ...], target_delay: int) -> Optional[List[Entry]]:
|
||||||
|
best_next_chain = None
|
||||||
|
best_next_lin = None
|
||||||
|
|
||||||
|
def dfs(chain_so_far: Tuple[Transition, ...], delay_so_far: int, current_state: str):
|
||||||
|
nonlocal best_next_chain, best_next_lin
|
||||||
|
|
||||||
|
# Base Case: Exact delay reached AND machine is in an accepting state
|
||||||
|
if delay_so_far == target_delay:
|
||||||
|
if current_state in self.accept_states:
|
||||||
|
lin = tuple(p for t in chain_so_far for p in t.linearize())
|
||||||
|
if lin > current_lin:
|
||||||
|
if best_next_lin is None or lin < best_next_lin:
|
||||||
|
best_next_lin = lin
|
||||||
|
best_next_chain = chain_so_far
|
||||||
|
return
|
||||||
|
|
||||||
|
if delay_so_far > target_delay:
|
||||||
|
return
|
||||||
|
|
||||||
|
p_lin = tuple(p for t in chain_so_far for p in t.linearize())
|
||||||
|
|
||||||
|
# Prefix Pruning
|
||||||
|
if self._check_divergence(p_lin, current_lin) == -1: return
|
||||||
|
if best_next_lin and self._check_divergence(p_lin, best_next_lin) == 1: return
|
||||||
|
|
||||||
|
for t in self.adj_list.get(current_state, []):
|
||||||
|
dfs(chain_so_far + (t,), delay_so_far + t.total_delay(), t.to_state)
|
||||||
|
|
||||||
|
dfs((), 0, self.start_state)
|
||||||
|
|
||||||
|
if best_next_chain:
|
||||||
|
return [entry for t in best_next_chain for entry in t.entries]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def parse_names(self, names: List[str]) -> List[Entry]:
|
||||||
|
"""
|
||||||
|
Takes a sequence of component names and infers their priorities and delays
|
||||||
|
by walking the state machine.
|
||||||
|
"""
|
||||||
|
current_state = self.start_state
|
||||||
|
i = 0
|
||||||
|
inferred_chain = []
|
||||||
|
|
||||||
|
while i < len(names):
|
||||||
|
match_found = False
|
||||||
|
|
||||||
|
# Look at all valid transitions from our current state
|
||||||
|
for t in self.adj_list.get(current_state, []):
|
||||||
|
# Extract the names of the components in this transition
|
||||||
|
t_names = [e.name for e in t.entries]
|
||||||
|
|
||||||
|
# Check if this transition matches the next components in our input
|
||||||
|
if names[i : i + len(t_names)] == t_names:
|
||||||
|
inferred_chain.extend(t.entries)
|
||||||
|
current_state = t.to_state
|
||||||
|
i += len(t_names)
|
||||||
|
match_found = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not match_found:
|
||||||
|
raise ValueError(
|
||||||
|
f"Syntax Error: Cannot place '{names[i]}' while in state '{current_state}' "
|
||||||
|
f"at index {i}."
|
||||||
|
)
|
||||||
|
|
||||||
|
if current_state not in self.accept_states:
|
||||||
|
raise ValueError(
|
||||||
|
f"Unexpected EOF: Sequence ended in non-accepting state '{current_state}'. "
|
||||||
|
"Did you forget to un-invert a torch?"
|
||||||
|
)
|
||||||
|
|
||||||
|
return inferred_chain
|
||||||
|
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# 1. GENERATE THE CARTESIAN STATE MACHINE
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
transitions = []
|
||||||
|
|
||||||
|
for signal in ["NORMAL", "INVERTED"]:
|
||||||
|
for faces in ["OTHER", "DIODE"]:
|
||||||
|
current_state = f"{signal}_{faces}"
|
||||||
|
|
||||||
|
# A. Wire (Zero-delay state reset. Breaks diode chains)
|
||||||
|
# FIX: Only allow wire if we are actually facing a diode.
|
||||||
|
if faces == "DIODE":
|
||||||
|
transitions.append(Transition(
|
||||||
|
from_state=current_state,
|
||||||
|
to_state=f"{signal}_OTHER",
|
||||||
|
entries=(Entry(0, 0, 'wire'),)
|
||||||
|
))
|
||||||
|
|
||||||
|
# B. Comparator
|
||||||
|
cmp_pri = -1 if faces == "DIODE" else 0
|
||||||
|
transitions.append(Transition(
|
||||||
|
from_state=current_state,
|
||||||
|
to_state=f"{signal}_DIODE",
|
||||||
|
entries=(Entry(cmp_pri, 2, 'cmp'),)
|
||||||
|
))
|
||||||
|
|
||||||
|
# C. Repeaters
|
||||||
|
for d in (2, 4, 6, 8):
|
||||||
|
if faces == "DIODE":
|
||||||
|
rep_pri = -3
|
||||||
|
else:
|
||||||
|
rep_pri = -1 if signal == "NORMAL" else -2
|
||||||
|
|
||||||
|
transitions.append(Transition(
|
||||||
|
from_state=current_state,
|
||||||
|
to_state=f"{signal}_DIODE",
|
||||||
|
entries=(Entry(rep_pri, d, f'rep{d}'),)
|
||||||
|
))
|
||||||
|
|
||||||
|
# D. Torch (Inverts signal, is not a diode)
|
||||||
|
next_sig = "INVERTED" if signal == "NORMAL" else "NORMAL"
|
||||||
|
transitions.append(Transition(
|
||||||
|
from_state=current_state,
|
||||||
|
to_state=f"{next_sig}_OTHER",
|
||||||
|
entries=(Entry(0, 2, 'torch'),)
|
||||||
|
))
|
||||||
|
|
||||||
|
# E. Fluids (Only allowed on rising edges/NORMAL)
|
||||||
|
if signal == "NORMAL":
|
||||||
|
for d in (5, 10, 30):
|
||||||
|
fluid_macro = (Entry(0, 2, 'obs'), Entry(1, d, f'fluid{d}'), Entry(0, 4, 'disp'))
|
||||||
|
transitions.append(Transition(
|
||||||
|
from_state=current_state,
|
||||||
|
to_state="NORMAL_OTHER",
|
||||||
|
entries=fluid_macro
|
||||||
|
))
|
||||||
|
|
||||||
|
# Initialize assuming the end of the chain faces nothing (OTHER).
|
||||||
|
# We require the final accepting state to be NORMAL (un-inverted) to be valid.
|
||||||
|
model = TilesetFSM(
|
||||||
|
transitions=transitions,
|
||||||
|
start_state='NORMAL_OTHER',
|
||||||
|
accept_states={'NORMAL_OTHER', 'NORMAL_DIODE'}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# 2. USAGE EXAMPLES
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
def display_chain(entries):
|
||||||
|
# Print the chain, highlighting the name and generated priority
|
||||||
|
return " <- ".join([f"{e.name}({e.priority})" for e in entries])
|
||||||
|
|
||||||
|
def linearize_entries(entries):
|
||||||
|
return tuple(p for e in entries for p in e.linear_parts())
|
||||||
|
|
||||||
|
# if __name__ == "__main__":
|
||||||
|
# # Example 1: The Zero-Delay Wire breaking the context
|
||||||
|
# chain_names_1 = ["cmp", "wire", "cmp"]
|
||||||
|
# entries_1 = model.parse_names(chain_names_1)
|
||||||
|
#
|
||||||
|
# print(f"Input: {chain_names_1}")
|
||||||
|
# print(f"Inferred: {display_chain(entries_1)}")
|
||||||
|
# # Output: cmp(0) -> wire(0) -> cmp(0)
|
||||||
|
# print()
|
||||||
|
#
|
||||||
|
# # Example 2: Diode-facing-diode context
|
||||||
|
# chain_names_2 = ["cmp", "cmp"]
|
||||||
|
# entries_2 = model.parse_names(chain_names_2)
|
||||||
|
#
|
||||||
|
# print(f"Input: {chain_names_2}")
|
||||||
|
# print(f"Inferred: {display_chain(entries_2)}")
|
||||||
|
# # Output: cmp(0) -> cmp(-1)
|
||||||
|
# print()
|
||||||
|
#
|
||||||
|
# # Example 3: Falling-edge inversion behavior
|
||||||
|
# chain_names_3 = ["torch", "rep4", "wire", "rep4", "torch"]
|
||||||
|
# entries_3 = model.parse_names(chain_names_3)
|
||||||
|
#
|
||||||
|
# print(f"Input: {chain_names_3}")
|
||||||
|
# print(f"Inferred: {display_chain(entries_3)}")
|
||||||
|
# # Output: torch(0) -> rep4(-3) -> wire(0) -> rep4(-2) -> torch(0)
|
||||||
|
# # (Notice the first rep4 faces the torch (a diode), getting -3,
|
||||||
|
# # while the second faces the wire (OTHER) during an inverted state, getting -2)
|
||||||
|
# print()
|
||||||
|
#
|
||||||
|
# # Example 4: Compiling a sequence to use as a search target
|
||||||
|
# target_names = ["cmp", "cmp", "cmp", "cmp"]
|
||||||
|
# target_entries = model.parse_names(target_names)
|
||||||
|
# target_lin = linearize_entries(target_entries)
|
||||||
|
#
|
||||||
|
# print(f"Finding NEXT chain for target: {target_names}")
|
||||||
|
# print(f"Inferred: {display_chain(target_entries)}")
|
||||||
|
# next_chain = model.find_next(target_lin, target_delay=8)
|
||||||
|
# if next_chain:
|
||||||
|
# print(f"Next: {display_chain(next_chain)}")
|
||||||
|
# else:
|
||||||
|
# print(f'Next: None')
|
||||||
|
# # Output: Next: rep2(-1) -> rep2(-3) -> rep2(-3) -> rep2(-3)
|
||||||
|
|
||||||
|
def enumerate_chains(model: TilesetFSM, target_delay: int):
|
||||||
|
"""
|
||||||
|
Yields every valid tile sequence of the target delay in lexicographical order.
|
||||||
|
"""
|
||||||
|
# Bootstrap with an infinitely low sequence to find the very first chain
|
||||||
|
current_lin = (-999,) * target_delay
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# Find the next valid sequence
|
||||||
|
next_chain = model.find_next(current_lin, target_delay)
|
||||||
|
|
||||||
|
# If no next sequence exists, we've enumerated the entire language
|
||||||
|
if next_chain is None:
|
||||||
|
break
|
||||||
|
|
||||||
|
yield next_chain
|
||||||
|
|
||||||
|
# Update our pointer for the next iteration
|
||||||
|
current_lin = linearize_entries(next_chain)
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# USAGE EXAMPLE
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# if __name__ == "__main__":
|
||||||
|
# target_delay = 6
|
||||||
|
# print(f"Enumerating all valid chains of Delay {target_delay}...\n")
|
||||||
|
#
|
||||||
|
# chain_count = 0
|
||||||
|
# for chain in enumerate_chains(model, target_delay):
|
||||||
|
# chain_count += 1
|
||||||
|
#
|
||||||
|
# # We can extract the names to see the raw component sequence
|
||||||
|
# names = [e.name for e in chain]
|
||||||
|
# print(f"{chain_count: 3d}. {names}")
|
||||||
|
#
|
||||||
|
# print(f"Total sequences found for delay {target_delay}: {chain_count}")
|
||||||
|
|
||||||
|
|
||||||
|
import heapq
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
def generate_render_document(chain_entries: List['Entry']) -> str:
|
||||||
|
"""
|
||||||
|
Generates a setblock-style animation document for a given redstone chain.
|
||||||
|
"""
|
||||||
|
# Reverse chain so x=0 is the first component to activate (upstream)
|
||||||
|
components = chain_entries[::-1]
|
||||||
|
N = len(components)
|
||||||
|
|
||||||
|
# 1. Forward sweep to calculate resting states (t=0)
|
||||||
|
resting_power = False
|
||||||
|
resting_states = []
|
||||||
|
for c in components:
|
||||||
|
if c.name == 'torch':
|
||||||
|
# Torches invert the resting power for downstream components
|
||||||
|
resting_power = not resting_power
|
||||||
|
resting_states.append(resting_power)
|
||||||
|
elif c.name == 'obs':
|
||||||
|
# Observers block resting power
|
||||||
|
resting_power = False
|
||||||
|
resting_states.append(False)
|
||||||
|
else:
|
||||||
|
# rep, cmp, and wire pass resting power
|
||||||
|
resting_states.append(resting_power)
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
|
||||||
|
actual_visual_state = {}
|
||||||
|
posmap = {}
|
||||||
|
x = 0
|
||||||
|
for i, (c, resting) in enumerate(zip(components, resting_states)):
|
||||||
|
is_powered = "true" if resting else "false"
|
||||||
|
|
||||||
|
if c.name == 'wire':
|
||||||
|
lines.append(f"p {x} 0 0 white_concrete")
|
||||||
|
lines.append(f"p {x} -1 0 smooth_stone_slab type=top")
|
||||||
|
elif c.name == 'torch':
|
||||||
|
lines.append(f"p {x} 0 0 white_concrete")
|
||||||
|
lines.append(f"p {x} -1 0 smooth_stone_slab type=top")
|
||||||
|
x += 1
|
||||||
|
lines.append(f"p {x} 0 0 redstone_wall_torch facing=east lit={is_powered}")
|
||||||
|
lines.append(f"p {x} -1 0 smooth_stone_slab type=top")
|
||||||
|
actual_visual_state[(i, 'lit')] = is_powered
|
||||||
|
posmap[i] = x
|
||||||
|
elif c.name.startswith('rep'):
|
||||||
|
delay_ticks = int(c.name[3:]) // 2
|
||||||
|
lines.append(f"p {x} 0 0 repeater facing=west powered={is_powered} locked=false delay={delay_ticks}")
|
||||||
|
lines.append(f"p {x} -1 0 smooth_stone_slab type=top")
|
||||||
|
actual_visual_state[(i, 'powered')] = is_powered
|
||||||
|
posmap[i] = x
|
||||||
|
elif c.name == 'cmp':
|
||||||
|
lines.append(f"p {x} 0 0 comparator facing=west powered={is_powered} mode=compare")
|
||||||
|
lines.append(f"p {x} -1 0 smooth_stone_slab type=top")
|
||||||
|
actual_visual_state[(i, 'powered')] = is_powered
|
||||||
|
posmap[i] = x
|
||||||
|
elif c.name == 'obs':
|
||||||
|
lines.append(f"p {x} 0 0 observer facing=west powered=false")
|
||||||
|
lines.append(f"p {x} -1 0 smooth_stone_slab type=top")
|
||||||
|
actual_visual_state[(i, 'powered')] = "false"
|
||||||
|
posmap[i] = x
|
||||||
|
|
||||||
|
x += 1
|
||||||
|
|
||||||
|
# 4. Simulate a 2gt pulse passing through the system
|
||||||
|
# pq holds (time, component_index, input_level)
|
||||||
|
pq = []
|
||||||
|
heapq.heappush(pq, (0, 0, True))
|
||||||
|
heapq.heappush(pq, (2, 0, False))
|
||||||
|
|
||||||
|
visual_changes = {} # time -> dict of (index, prop) -> value
|
||||||
|
|
||||||
|
while pq:
|
||||||
|
t, i, level = heapq.heappop(pq)
|
||||||
|
|
||||||
|
if i >= N:
|
||||||
|
continue
|
||||||
|
|
||||||
|
c = components[i]
|
||||||
|
|
||||||
|
if c.name == 'wire':
|
||||||
|
# Instantly passes the signal to the next component
|
||||||
|
heapq.heappush(pq, (t, i+1, level))
|
||||||
|
|
||||||
|
elif c.name == 'torch':
|
||||||
|
out_level = not level
|
||||||
|
out_t = t + 2
|
||||||
|
heapq.heappush(pq, (out_t, i+1, out_level))
|
||||||
|
|
||||||
|
val_str = "true" if out_level else "false"
|
||||||
|
if out_t not in visual_changes: visual_changes[out_t] = {}
|
||||||
|
visual_changes[out_t][(i, 'lit')] = val_str
|
||||||
|
|
||||||
|
elif c.name.startswith('rep'):
|
||||||
|
delay = int(c.name[3:])
|
||||||
|
out_t = t + delay
|
||||||
|
heapq.heappush(pq, (out_t, i+1, level))
|
||||||
|
|
||||||
|
val_str = "true" if level else "false"
|
||||||
|
if out_t not in visual_changes: visual_changes[out_t] = {}
|
||||||
|
visual_changes[out_t][(i, 'powered')] = val_str
|
||||||
|
|
||||||
|
elif c.name == 'cmp':
|
||||||
|
out_t = t + 2
|
||||||
|
heapq.heappush(pq, (out_t, i+1, level))
|
||||||
|
|
||||||
|
val_str = "true" if level else "false"
|
||||||
|
if out_t not in visual_changes: visual_changes[out_t] = {}
|
||||||
|
visual_changes[out_t][(i, 'powered')] = val_str
|
||||||
|
|
||||||
|
elif c.name == 'obs':
|
||||||
|
# An observer fires a 2gt pulse whenever its input changes
|
||||||
|
out_t_on = t + 2
|
||||||
|
out_t_off = t + 4
|
||||||
|
|
||||||
|
heapq.heappush(pq, (out_t_on, i+1, True))
|
||||||
|
heapq.heappush(pq, (out_t_off, i+1, False))
|
||||||
|
|
||||||
|
if out_t_on not in visual_changes: visual_changes[out_t_on] = {}
|
||||||
|
if out_t_off not in visual_changes: visual_changes[out_t_off] = {}
|
||||||
|
|
||||||
|
# Python's heapq pops `False` before `True` if times match.
|
||||||
|
# This perfectly replicates observer pulse-extension: if an OFF and ON
|
||||||
|
# hit on the exact same tick, the ON overwrites the OFF.
|
||||||
|
visual_changes[out_t_on][(i, 'powered')] = "true"
|
||||||
|
visual_changes[out_t_off][(i, 'powered')] = "false"
|
||||||
|
|
||||||
|
# 5. Format the dynamic animation frames
|
||||||
|
sorted_times = sorted(visual_changes.keys())
|
||||||
|
|
||||||
|
for t in sorted_times:
|
||||||
|
frame_lines = []
|
||||||
|
changes_at_t = visual_changes[t]
|
||||||
|
|
||||||
|
for (i, prop), val in changes_at_t.items():
|
||||||
|
x = posmap[i]
|
||||||
|
# Only emit if it actually changes the visual state
|
||||||
|
if actual_visual_state.get((i, prop)) != val:
|
||||||
|
actual_visual_state[(i, prop)] = val
|
||||||
|
frame_lines.append(f"p {x} 0 0 {prop}={val}")
|
||||||
|
|
||||||
|
if frame_lines:
|
||||||
|
lines.append(f"t {t}")
|
||||||
|
lines.extend(frame_lines)
|
||||||
|
|
||||||
|
# 6. Add final padding frame
|
||||||
|
last_t = sorted_times[-1] if sorted_times else 0
|
||||||
|
lines.append(f"t {last_t + 4}")
|
||||||
|
lines.append("p 0 0 0")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def makedoc(chain: list[Entry], z=0):
|
||||||
|
x = 0
|
||||||
|
|
||||||
|
for el in chain:
|
||||||
|
match el.name:
|
||||||
|
case 'cmp':
|
||||||
|
yield f'p {x} 0 {z} comparator facing=west powered=false mode=compare'
|
||||||
|
yield f'p {x} -1 {z} smooth_stone_slab type=top'
|
||||||
|
case 'rep2':
|
||||||
|
yield f'p {x} 0 {z} repeater facing=west powered=false locked=false delay=1'
|
||||||
|
yield f'p {x} -1 {z} smooth_stone_slab type=top'
|
||||||
|
case 'rep4':
|
||||||
|
yield f'p {x} 0 {z} repeater facing=west powered=false locked=false delay=2'
|
||||||
|
yield f'p {x} -1 {z} smooth_stone_slab type=top'
|
||||||
|
case 'rep6':
|
||||||
|
yield f'p {x} 0 {z} repeater facing=west powered=false locked=false delay=3'
|
||||||
|
yield f'p {x} -1 {z} smooth_stone_slab type=top'
|
||||||
|
case 'rep8':
|
||||||
|
yield f'p {x} 0 {z} repeater facing=west powered=false locked=false delay=4'
|
||||||
|
yield f'p {x} -1 {z} smooth_stone_slab type=top'
|
||||||
|
case 'wire':
|
||||||
|
yield f'p {x} 0 {z} white_concrete'
|
||||||
|
yield f'p {x} -1 {z} smooth_stone_slab type=top'
|
||||||
|
case 'torch':
|
||||||
|
yield f'p {x} 0 {z} redstone_wall_torch facing=east lit=false'
|
||||||
|
yield f'p {x} -1 {z} smooth_stone_slab type=top'
|
||||||
|
x -= 1
|
||||||
|
yield f'p {x} 0 {z} white_concrete'
|
||||||
|
yield f'p {x} -1 {z} smooth_stone_slab type=top'
|
||||||
|
x -= 1
|
||||||
|
|
||||||
|
|
||||||
|
# return ' '.join(el.name for el in chain)
|
||||||
|
|
||||||
|
# for el in chain:
|
||||||
|
# match el.name:
|
||||||
|
# print(el.name)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
target_delay = 6
|
||||||
|
|
||||||
|
with open('/home/local/KHQ/david.allemang/src/wireless/content/tilesets.typ', 'w') as f:
|
||||||
|
with redirect_stdout(f):
|
||||||
|
print('''
|
||||||
|
#import "/lib.typ": diorama, example, note, todo
|
||||||
|
|
||||||
|
== Tilesets <tilesets>
|
||||||
|
|
||||||
|
#show raw.where(lang: "mc-diorama"): it => diorama(
|
||||||
|
zoom: true,
|
||||||
|
theta: 180,
|
||||||
|
phi: 60,
|
||||||
|
radius: 30,
|
||||||
|
height: "120em",
|
||||||
|
it.text,
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
z = 0
|
||||||
|
|
||||||
|
print('```mc-diorama')
|
||||||
|
for chain in enumerate_chains(model, target_delay):
|
||||||
|
for line in makedoc(chain, z):
|
||||||
|
print(line)
|
||||||
|
|
||||||
|
z -= 1.25
|
||||||
|
print('```')
|
||||||
169
grammar.py
Normal file
169
grammar.py
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Tuple, Optional, List
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Entry:
|
||||||
|
priority: int
|
||||||
|
delay: int
|
||||||
|
name: str = '-'
|
||||||
|
|
||||||
|
def linear_parts(self) -> Tuple[int, ...]:
|
||||||
|
return (self.priority,) * self.delay
|
||||||
|
|
||||||
|
# A ComponentBlock is an atomic sequence of Entries that can be physically built.
|
||||||
|
ComponentBlock = Tuple[Entry, ...]
|
||||||
|
|
||||||
|
def block_linearize(block: ComponentBlock) -> Tuple[int, ...]:
|
||||||
|
"""Linearizes a single macro-block."""
|
||||||
|
return tuple(p for e in block for p in e.linear_parts())
|
||||||
|
|
||||||
|
def chain_linearize(chain: Tuple[ComponentBlock, ...]) -> Tuple[int, ...]:
|
||||||
|
"""Linearizes a full sequence of blocks."""
|
||||||
|
return tuple(p for b in chain for p in block_linearize(b))
|
||||||
|
|
||||||
|
def flatten_chain(chain: Tuple[ComponentBlock, ...]) -> List[Entry]:
|
||||||
|
"""Converts the internal tuple-based chain back to your flat List[Entry] format."""
|
||||||
|
return [entry for block in chain for entry in block]
|
||||||
|
|
||||||
|
|
||||||
|
class TilesetModel:
|
||||||
|
def __init__(self, allowed_blocks: List[ComponentBlock]):
|
||||||
|
self.blocks = allowed_blocks
|
||||||
|
|
||||||
|
def _check_divergence(self, prefix: Tuple[int, ...], target: Tuple[int, ...]) -> int:
|
||||||
|
"""
|
||||||
|
Returns -1 if prefix is lexicographically < target,
|
||||||
|
1 if prefix is lexicographically > target,
|
||||||
|
0 if prefix is a strict prefix of target (no divergence yet).
|
||||||
|
"""
|
||||||
|
for p_val, t_val in zip(prefix, target):
|
||||||
|
if p_val < t_val: return -1
|
||||||
|
if p_val > t_val: return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def find_next(self, current_chain: Tuple[ComponentBlock, ...]) -> Optional[Tuple[ComponentBlock, ...]]:
|
||||||
|
"""Finds the lexicographically next chain that perfectly matches the total delay."""
|
||||||
|
target_delay = sum(e.delay for b in current_chain for e in b)
|
||||||
|
current_lin = chain_linearize(current_chain)
|
||||||
|
|
||||||
|
best_next_chain = None
|
||||||
|
best_next_lin = None
|
||||||
|
|
||||||
|
def dfs(chain_so_far: Tuple[ComponentBlock, ...], delay_so_far: int):
|
||||||
|
nonlocal best_next_chain, best_next_lin
|
||||||
|
|
||||||
|
# Base Case: Exact delay reached
|
||||||
|
if delay_so_far == target_delay:
|
||||||
|
lin = chain_linearize(chain_so_far)
|
||||||
|
if lin > current_lin:
|
||||||
|
if best_next_lin is None or lin < best_next_lin:
|
||||||
|
best_next_lin = lin
|
||||||
|
best_next_chain = chain_so_far
|
||||||
|
return
|
||||||
|
|
||||||
|
# Base Case: Overshot delay limits
|
||||||
|
if delay_so_far > target_delay:
|
||||||
|
return
|
||||||
|
|
||||||
|
p_lin = chain_linearize(chain_so_far)
|
||||||
|
|
||||||
|
# PRUNING 1: If prefix diverges and is strictly LESS than current_lin,
|
||||||
|
# any suffix appended to it will also be strictly less. Prune the branch.
|
||||||
|
if self._check_divergence(p_lin, current_lin) == -1:
|
||||||
|
return
|
||||||
|
|
||||||
|
# PRUNING 2: If prefix diverges and is strictly GREATER than the best_next_lin
|
||||||
|
# we've already found, any suffix will also be greater. It can't beat our current best.
|
||||||
|
if best_next_lin is not None:
|
||||||
|
if self._check_divergence(p_lin, best_next_lin) == 1:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Branching
|
||||||
|
for b in self.blocks:
|
||||||
|
dfs(chain_so_far + (b,), delay_so_far + sum(e.delay for e in b))
|
||||||
|
|
||||||
|
dfs((), 0)
|
||||||
|
return best_next_chain
|
||||||
|
|
||||||
|
def find_previous(self, current_chain: Tuple[ComponentBlock, ...]) -> Optional[Tuple[ComponentBlock, ...]]:
|
||||||
|
"""Finds the lexicographically previous chain matching total delay."""
|
||||||
|
target_delay = sum(e.delay for b in current_chain for e in b)
|
||||||
|
current_lin = chain_linearize(current_chain)
|
||||||
|
|
||||||
|
best_prev_chain = None
|
||||||
|
best_prev_lin = None
|
||||||
|
|
||||||
|
def dfs(chain_so_far: Tuple[ComponentBlock, ...], delay_so_far: int):
|
||||||
|
nonlocal best_prev_chain, best_prev_lin
|
||||||
|
|
||||||
|
if delay_so_far == target_delay:
|
||||||
|
lin = chain_linearize(chain_so_far)
|
||||||
|
if lin < current_lin:
|
||||||
|
if best_prev_lin is None or lin > best_prev_lin:
|
||||||
|
best_prev_lin = lin
|
||||||
|
best_prev_chain = chain_so_far
|
||||||
|
return
|
||||||
|
if delay_so_far > target_delay:
|
||||||
|
return
|
||||||
|
|
||||||
|
p_lin = chain_linearize(chain_so_far)
|
||||||
|
|
||||||
|
# Prune if prefix diverges and is strictly GREATER than current_lin
|
||||||
|
if self._check_divergence(p_lin, current_lin) == 1:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Prune if prefix diverges and is strictly LESS than best_prev_lin
|
||||||
|
if best_prev_lin is not None:
|
||||||
|
if self._check_divergence(p_lin, best_prev_lin) == -1:
|
||||||
|
return
|
||||||
|
|
||||||
|
for b in self.blocks:
|
||||||
|
dfs(chain_so_far + (b,), delay_so_far + sum(e.delay for e in b))
|
||||||
|
|
||||||
|
dfs((), 0)
|
||||||
|
return best_prev_chain
|
||||||
|
|
||||||
|
# --- Define the "Alphabet" of valid Macro-Blocks ---
|
||||||
|
|
||||||
|
# Standard Components
|
||||||
|
repeaters = [(Entry(p, d, f'rep({p},{d})'),) for p in (-1, -3) for d in (2, 4, 6, 8)]
|
||||||
|
comparators = [(Entry(p, 2, f'cmp({p})'),) for p in (0, -1)]
|
||||||
|
others = [(Entry(0, 2, 'other'),)]
|
||||||
|
|
||||||
|
# Subsequences: Signal Inverted Repeaters
|
||||||
|
# (Note: Updates flow right-to-left. Torch2 updates latest so it goes on the left)
|
||||||
|
inv_repeaters = [
|
||||||
|
(Entry(0, 2, 'torch2'), Entry(p, d, f'inv_rep({p},{d})'), Entry(0, 2, 'torch1'))
|
||||||
|
for p in (-2, -3) for d in (2, 4, 6, 8)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Subsequences: Fluids & Observers
|
||||||
|
# (Observer updates latest so it goes on the left)
|
||||||
|
fluids = [
|
||||||
|
(Entry(0, 2, 'obs'), Entry(1, d, f'fluid({d})'), Entry(0, 4, 'disp'))
|
||||||
|
for d in (5, 10, 30)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Compile the generative grammar
|
||||||
|
ALL_MACRO_BLOCKS = repeaters + comparators + others + inv_repeaters + fluids
|
||||||
|
|
||||||
|
# --- Usage Example ---
|
||||||
|
|
||||||
|
model = TilesetModel(ALL_MACRO_BLOCKS)
|
||||||
|
|
||||||
|
# Let's say we have a specific chain of Total Delay = 14
|
||||||
|
current = (
|
||||||
|
(Entry(0, 2, 'cmp(0)'),),
|
||||||
|
(Entry(-3, 8, 'rep(-3,8)'),),
|
||||||
|
(Entry(-1, 4, 'rep(-1,4)'),)
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Current Chain: {flatten_chain(current)}")
|
||||||
|
|
||||||
|
next_chain = model.find_next(current)
|
||||||
|
if next_chain:
|
||||||
|
print(f"Next Chain: {flatten_chain(next_chain)}")
|
||||||
|
|
||||||
|
prev_chain = model.find_previous(current)
|
||||||
|
if prev_chain:
|
||||||
|
print(f"Prev Chain: {flatten_chain(prev_chain)}")
|
||||||
104
permute.py
Normal file
104
permute.py
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from pprint import pprint
|
||||||
|
from random import shuffle
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
|
# Here we model tilesets as "chains"; sequences of named components and their priority-delay pairs.
|
||||||
|
# Components are processed via a priority queue; so on each tick, components which are scheduled to update will be processed in priority order.
|
||||||
|
# When components update, they trigger the following component to update after some delay. Then, in the future, that component is updated according to its priority and the process continues down the chain.
|
||||||
|
# If two components update in the same tick with the same priority, then they do so in the order in which they were scheduled.
|
||||||
|
|
||||||
|
# Note that components which update later in time have stronger significance on the order of execution; so for our notation we place these on the left-hand-side (first in lists) to match left-to-right lexicographic notation.
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Entry:
|
||||||
|
priority: int
|
||||||
|
delay: int
|
||||||
|
name: str = '-'
|
||||||
|
|
||||||
|
# We can model this by linearizing these tuples into tuples, then lexicographically sort these linearized keys.
|
||||||
|
# We directly sort on priority: more-negative priorities update before more-positive priorities, so these are correctly sorted lexicographically. However we only want this behavior in the case that the components update at the same time, so we can 'pad out' the effect of delay by repeating the priority.
|
||||||
|
|
||||||
|
def linear_parts(entry: Entry):
|
||||||
|
assert entry.delay > 0
|
||||||
|
# return (-np.inf,) * (entry.delay - 1) + (entry.priority,)
|
||||||
|
return (entry.priority,) * entry.delay
|
||||||
|
|
||||||
|
def linearize(entries: Iterable[Entry]):
|
||||||
|
return [part for entry in entries for part in linear_parts(entry)]
|
||||||
|
|
||||||
|
def sort(chains: list[Entry]):
|
||||||
|
return sorted(chains, key=linearize)
|
||||||
|
|
||||||
|
# one framework to produce a tileset is to encode binary values by comparator and repeater. all cases have equal delay, so it just falls to the priorities. comparators have priority 0 at the end and -1 in the middle, while repeaters have priority -1 at the end and -2 in the middle.
|
||||||
|
|
||||||
|
chains = [
|
||||||
|
[Entry(-0, 2, 'cmp'), Entry(-1, 2, 'cmp'), Entry(-1, 2, 'cmp'), Entry(-1, 2, 'cmp')],
|
||||||
|
[Entry(-0, 2, 'cmp'), Entry(-1, 2, 'cmp'), Entry(-1, 2, 'cmp'), Entry(-2, 2, 'rep')],
|
||||||
|
[Entry(-0, 2, 'cmp'), Entry(-1, 2, 'cmp'), Entry(-2, 2, 'rep'), Entry(-1, 2, 'cmp')],
|
||||||
|
[Entry(-0, 2, 'cmp'), Entry(-1, 2, 'cmp'), Entry(-2, 2, 'rep'), Entry(-2, 2, 'rep')],
|
||||||
|
[Entry(-0, 2, 'cmp'), Entry(-3, 2, 'rep'), Entry(-1, 2, 'cmp'), Entry(-1, 2, 'cmp')],
|
||||||
|
[Entry(-0, 2, 'cmp'), Entry(-3, 2, 'rep'), Entry(-1, 2, 'cmp'), Entry(-3, 2, 'rep')],
|
||||||
|
[Entry(-0, 2, 'cmp'), Entry(-3, 2, 'rep'), Entry(-3, 2, 'rep'), Entry(-1, 2, 'cmp')],
|
||||||
|
[Entry(-0, 2, 'cmp'), Entry(-3, 2, 'rep'), Entry(-3, 2, 'rep'), Entry(-3, 2, 'rep')],
|
||||||
|
[Entry(-1, 2, 'rep'), Entry(-1, 2, 'cmp'), Entry(-1, 2, 'cmp'), Entry(-1, 2, 'cmp')],
|
||||||
|
[Entry(-1, 2, 'rep'), Entry(-1, 2, 'cmp'), Entry(-1, 2, 'cmp'), Entry(-3, 2, 'rep')],
|
||||||
|
[Entry(-1, 2, 'rep'), Entry(-1, 2, 'cmp'), Entry(-3, 2, 'rep'), Entry(-1, 2, 'cmp')],
|
||||||
|
[Entry(-1, 2, 'rep'), Entry(-1, 2, 'cmp'), Entry(-3, 2, 'rep'), Entry(-3, 2, 'rep')],
|
||||||
|
[Entry(-1, 2, 'rep'), Entry(-3, 2, 'rep'), Entry(-1, 2, 'cmp'), Entry(-1, 2, 'cmp')],
|
||||||
|
[Entry(-1, 2, 'rep'), Entry(-3, 2, 'rep'), Entry(-1, 2, 'cmp'), Entry(-3, 2, 'rep')],
|
||||||
|
[Entry(-1, 2, 'rep'), Entry(-3, 2, 'rep'), Entry(-3, 2, 'rep'), Entry(-1, 2, 'cmp')],
|
||||||
|
[Entry(-1, 2, 'rep'), Entry(-3, 2, 'rep'), Entry(-3, 2, 'rep'), Entry(-3, 2, 'rep')],
|
||||||
|
]
|
||||||
|
shuffle(chains)
|
||||||
|
|
||||||
|
# another framework is to use permutations of repeaters on varying delay. now all cases have equal priority, so it just falls to the delays.
|
||||||
|
|
||||||
|
chains = [
|
||||||
|
[Entry(-1, 8, 'r4'), Entry(-3, 6, 'r3'), Entry(-3, 4, 'r2'), Entry(-3, 2, 'r1')],
|
||||||
|
[Entry(-1, 8, 'r4'), Entry(-3, 6, 'r3'), Entry(-3, 2, 'r1'), Entry(-3, 4, 'r2')],
|
||||||
|
[Entry(-1, 8, 'r4'), Entry(-3, 4, 'r2'), Entry(-3, 6, 'r3'), Entry(-3, 2, 'r1')],
|
||||||
|
[Entry(-1, 8, 'r4'), Entry(-3, 4, 'r2'), Entry(-3, 2, 'r1'), Entry(-3, 6, 'r3')],
|
||||||
|
[Entry(-1, 8, 'r4'), Entry(-3, 2, 'r1'), Entry(-3, 6, 'r3'), Entry(-3, 4, 'r2')],
|
||||||
|
[Entry(-1, 8, 'r4'), Entry(-3, 2, 'r1'), Entry(-3, 4, 'r2'), Entry(-3, 6, 'r3')],
|
||||||
|
[Entry(-1, 6, 'r3'), Entry(-3, 8, 'r4'), Entry(-3, 4, 'r2'), Entry(-3, 2, 'r1')],
|
||||||
|
[Entry(-1, 6, 'r3'), Entry(-3, 8, 'r4'), Entry(-3, 2, 'r1'), Entry(-3, 4, 'r2')],
|
||||||
|
[Entry(-1, 6, 'r3'), Entry(-3, 4, 'r2'), Entry(-3, 8, 'r4'), Entry(-3, 2, 'r1')],
|
||||||
|
[Entry(-1, 6, 'r3'), Entry(-3, 4, 'r2'), Entry(-3, 2, 'r1'), Entry(-3, 8, 'r4')],
|
||||||
|
[Entry(-1, 6, 'r3'), Entry(-3, 2, 'r1'), Entry(-3, 8, 'r4'), Entry(-3, 4, 'r2')],
|
||||||
|
[Entry(-1, 6, 'r3'), Entry(-3, 2, 'r1'), Entry(-3, 4, 'r2'), Entry(-3, 8, 'r4')],
|
||||||
|
[Entry(-1, 4, 'r2'), Entry(-3, 8, 'r4'), Entry(-3, 6, 'r3'), Entry(-3, 2, 'r1')],
|
||||||
|
[Entry(-1, 4, 'r2'), Entry(-3, 8, 'r4'), Entry(-3, 2, 'r1'), Entry(-3, 6, 'r3')],
|
||||||
|
[Entry(-1, 4, 'r2'), Entry(-3, 6, 'r3'), Entry(-3, 8, 'r4'), Entry(-3, 2, 'r1')],
|
||||||
|
[Entry(-1, 4, 'r2'), Entry(-3, 6, 'r3'), Entry(-3, 2, 'r1'), Entry(-3, 8, 'r4')],
|
||||||
|
[Entry(-1, 4, 'r2'), Entry(-3, 2, 'r1'), Entry(-3, 8, 'r4'), Entry(-3, 6, 'r3')],
|
||||||
|
[Entry(-1, 4, 'r2'), Entry(-3, 2, 'r1'), Entry(-3, 6, 'r3'), Entry(-3, 8, 'r4')],
|
||||||
|
[Entry(-1, 2, 'r1'), Entry(-3, 8, 'r4'), Entry(-3, 6, 'r3'), Entry(-3, 4, 'r2')],
|
||||||
|
[Entry(-1, 2, 'r1'), Entry(-3, 8, 'r4'), Entry(-3, 4, 'r2'), Entry(-3, 6, 'r3')],
|
||||||
|
[Entry(-1, 2, 'r1'), Entry(-3, 6, 'r3'), Entry(-3, 8, 'r4'), Entry(-3, 4, 'r2')],
|
||||||
|
[Entry(-1, 2, 'r1'), Entry(-3, 6, 'r3'), Entry(-3, 4, 'r2'), Entry(-3, 8, 'r4')],
|
||||||
|
[Entry(-1, 2, 'r1'), Entry(-3, 4, 'r2'), Entry(-3, 8, 'r4'), Entry(-3, 6, 'r3')],
|
||||||
|
[Entry(-1, 2, 'r1'), Entry(-3, 4, 'r2'), Entry(-3, 6, 'r3'), Entry(-3, 8, 'r4')],
|
||||||
|
]
|
||||||
|
shuffle(chains)
|
||||||
|
|
||||||
|
# However neither of these are the general case.
|
||||||
|
#
|
||||||
|
# Basically, we want a framework that can take a particular chain of (priority, delay) pairs and come up with the *next* (or previous) sequence given this `linearize` ordering using the (priority, delay) pairs which we can generate.
|
||||||
|
#
|
||||||
|
# the full list of generable pairs is described as follows.
|
||||||
|
#
|
||||||
|
# repeaters: [-1,-3] x [2,4,6,8]
|
||||||
|
# comparators: [-0,-1] x [2]
|
||||||
|
# all other components: [-0] x [2]
|
||||||
|
#
|
||||||
|
# it is in principle possible to generate sub-sequences of other tuples, however these require involved setup.
|
||||||
|
#
|
||||||
|
# by using redstone torches to invert a signal, it is possible to generate a subsequence involving priority -2. (While the signal is inverted, repeaters generate priorities [-2,-3] x [2,4,6,8])
|
||||||
|
# [(-0,2), (-2,2), (-0,2)]
|
||||||
|
#
|
||||||
|
# by using fluids and observers, it is possible to generate signals of delay 5, 30 (in the overworld) or delay 10 only (in the nether). fluids tick after tile ticks, so it is effectively a priority of +1.
|
||||||
|
# [(-0,2), (+1,5), (-0,4)] # the sequence here is a dispenser (-0,4) produces water (+1,5) which is observed by observer (-0,2).
|
||||||
|
|
||||||
|
# pprint([tuple(el.name for el in chain) for chain in chains])
|
||||||
|
pprint([tuple(el.name for el in chain) for chain in sort(chains)])
|
||||||
5
pyproject.toml
Normal file
5
pyproject.toml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
[project]
|
||||||
|
name = "wireless-formalism"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = ">=3.14"
|
||||||
|
dependencies = []
|
||||||
Reference in New Issue
Block a user