From 06569fd51f2cb2b45e553b89a2e60810affbfbea Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Thu, 9 Jan 2025 13:55:20 -0800 Subject: [PATCH 01/27] feat: initial gif procesing --- .../materials_designer/wave_js_research.ipynb | 345 ++++++++++++++++++ 1 file changed, 345 insertions(+) create mode 100644 other/materials_designer/wave_js_research.ipynb diff --git a/other/materials_designer/wave_js_research.ipynb b/other/materials_designer/wave_js_research.ipynb new file mode 100644 index 000000000..2bdc23d8a --- /dev/null +++ b/other/materials_designer/wave_js_research.ipynb @@ -0,0 +1,345 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# GIF Processing Notebook\n", + "\n", + "This notebook provides functionality to:\n", + "1. Resize GIFs to specified dimensions\n", + "2. Add text overlay at selected positions\n", + "3. Add image overlay at selected positions\n", + "4. Compress the resulting GIF" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Import required libraries\n", + "from PIL import Image, ImageDraw, ImageFont\n", + "import os\n", + "from IPython.display import display, Image as IPImage\n", + "import numpy as np" + ], + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "import matplotlib.font_manager as fm\n", + "class FontManager:\n", + " \"\"\"Manages fonts for the GIF processor\"\"\"\n", + "\n", + " def __init__(self):\n", + " self.fonts = {\n", + " 'default': None, # Will be initialized with Ubuntu font\n", + " 'matplotlib': {} # Will be populated with matplotlib fonts\n", + " }\n", + " self._initialize_fonts()\n", + "\n", + " def _initialize_fonts(self):\n", + " # Load built-in Ubuntu font\n", + " # Load matplotlib fonts\n", + " font_names = ['serif', 'sans-serif', 'monospace']\n", + " for name in font_names:\n", + " try:\n", + " font_path = fm.findfont(fm.FontProperties(family=name))\n", + " self.fonts['matplotlib'][name] = font_path\n", + " except:\n", + " continue\n", + "\n", + " def get_font(self, font_name='default', size=30):\n", + " \"\"\"Get a font by name and size\"\"\"\n", + " try:\n", + " if font_name == 'default':\n", + " return ImageFont.truetype(self.fonts['default'], size)\n", + " elif font_name in self.fonts['matplotlib']:\n", + " return ImageFont.truetype(self.fonts['matplotlib'][font_name], size)\n", + " else:\n", + " raise ValueError(f\"Font '{font_name}' not found\")\n", + " except Exception as e:\n", + " print(f\"Error loading font {font_name}: {str(e)}\")\n", + " return ImageFont.load_default()\n", + "\n", + " def list_fonts(self):\n", + " \"\"\"List all available fonts\"\"\"\n", + " available_fonts = ['default'] + list(self.fonts['matplotlib'].keys())\n", + " return available_fonts" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from io import BytesIO\n", + "\n", + "\n", + "class GIFProcessor:\n", + " def __init__(self, gif_path):\n", + " \"\"\"Initialize with path to GIF file\"\"\"\n", + " self.gif = Image.open(gif_path)\n", + " self.frames = []\n", + " self.durations = []\n", + "\n", + " # Extract all frames and their durations\n", + " try:\n", + " while True:\n", + " self.frames.append(self.gif.copy())\n", + " self.durations.append(self.gif.info.get('duration', 100))\n", + " self.gif.seek(self.gif.tell() + 1)\n", + " except EOFError:\n", + " pass\n", + "\n", + " def resize(self, width, height):\n", + " \"\"\"Resize all frames to specified dimensions\"\"\"\n", + " self.frames = [frame.resize((width, height), Image.Resampling.LANCZOS)\n", + " for frame in self.frames]\n", + " return self\n", + "\n", + " def make_square(self, size=None):\n", + " \"\"\"\n", + " Crop the GIF to a square from the center.\n", + " If size is provided, the output will be resized to size x size.\n", + " If size is None, the square will be sized to the smaller dimension.\n", + " \"\"\"\n", + " if not self.frames:\n", + " return self\n", + "\n", + " # Get dimensions from first frame\n", + " width, height = self.frames[0].size\n", + "\n", + " # Calculate crop box for square\n", + " if width > height:\n", + " # Landscape orientation\n", + " left = (width - height) // 2\n", + " top = 0\n", + " right = left + height\n", + " bottom = height\n", + " else:\n", + " # Portrait orientation\n", + " left = 0\n", + " top = (height - width) // 2\n", + " right = width\n", + " bottom = top + width\n", + "\n", + " # Apply crop to all frames\n", + " self.frames = [frame.crop((left, top, right, bottom)) for frame in self.frames]\n", + "\n", + " # Resize if size is specified\n", + " if size is not None:\n", + " self.frames = [frame.resize((size, size), Image.Resampling.LANCZOS)\n", + " for frame in self.frames]\n", + "\n", + " return self\n", + "\n", + " def add_text(self, text, position, font_path=None, font_size=30,\n", + " color=(255, 255, 255), stroke_width=2, stroke_fill=(0, 0, 0)):\n", + " \"\"\"Add text overlay to all frames\"\"\"\n", + " font_manager = FontManager()\n", + " font = font_manager.get_font(font_name=font_path,size=font_size)\n", + "\n", + " for i, frame in enumerate(self.frames):\n", + " # Convert to RGBA before drawing\n", + " frame_rgba = frame.convert('RGBA')\n", + " draw = ImageDraw.Draw(frame_rgba)\n", + " draw.text(position, text, font=font, fill=color,\n", + " stroke_width=stroke_width, stroke_fill=stroke_fill)\n", + " self.frames[i] = frame_rgba\n", + " return self\n", + "\n", + " def add_image_overlay(self, overlay_path, position):\n", + " \"\"\"Add image overlay to all frames\"\"\"\n", + " overlay = Image.open(overlay_path).convert('RGBA')\n", + "\n", + " for i, frame in enumerate(self.frames):\n", + " frame_rgba = frame.convert('RGBA')\n", + " frame_rgba.paste(overlay, position, overlay)\n", + " self.frames[i] = frame_rgba\n", + " return self\n", + "\n", + " def save(self, output_path, optimize=False, quality=100):\n", + " \"\"\"\n", + " Save the processed GIF with optimization options\n", + "\n", + " Args:\n", + " output_path (str): Path to save the GIF\n", + " optimize (bool): Whether to optimize the GIF\n", + " quality (int): Quality from 1 (worst) to 100 (best).\n", + " Lower quality means smaller file size.\n", + " \"\"\"\n", + " if not self.frames:\n", + " return\n", + "\n", + " # Convert frames to RGB mode for saving\n", + " rgb_frames = []\n", + " for frame in self.frames:\n", + " rgb_frame = frame.convert('RGB')\n", + "\n", + " if optimize:\n", + " # Calculate number of colors based on quality\n", + " n_colors = max(min(256, int(256 * (quality / 100))), 2)\n", + "\n", + " # Convert to P mode (palette) with optimized palette\n", + " rgb_frame = rgb_frame.quantize(\n", + " colors=n_colors,\n", + " method=Image.Quantize.MEDIANCUT,\n", + " dither=Image.Dither.FLOYDSTEINBERG\n", + " )\n", + "\n", + " rgb_frames.append(rgb_frame)\n", + "\n", + " # Save with optimization\n", + " rgb_frames[0].save(\n", + " output_path,\n", + " save_all=True,\n", + " append_images=rgb_frames[1:],\n", + " optimize=optimize,\n", + " duration=self.durations,\n", + " loop=0,\n", + " format='GIF',\n", + " # Additional optimization parameters\n", + " disposal=2, # Clear the frame before rendering the next\n", + " quality=quality\n", + " )\n", + " print(\"Size on disk:\", f\"{os.path.getsize(output_path) / 1024 / 1024:.2f} MB\")\n", + "\n", + " def display(self):\n", + " \"\"\"Display the current state of the GIF in the notebook\"\"\"\n", + " temp_path = '_temp_display.gif'\n", + " self.save(temp_path)\n", + " display(IPImage(filename=temp_path))\n", + " os.remove(temp_path)\n", + "\n", + " def get_size(self, optimize=False, quality=100):\n", + " \"\"\"Get the size of the processed GIF in bytes without saving to disk\"\"\"\n", + " if self.frames:\n", + " # Convert frames back to RGB mode for saving\n", + " rgb_frames = [frame.convert('RGB') for frame in self.frames]\n", + " with BytesIO() as buffer:\n", + " rgb_frames[0].save(\n", + " buffer,\n", + " save_all=True,\n", + " append_images=rgb_frames[1:],\n", + " optimize=optimize,\n", + " quality=quality,\n", + " duration=self.durations,\n", + " loop=0,\n", + " format='GIF'\n", + " )\n", + " return buffer.tell()" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Usage Example" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Example usage\n", + "GIF_PATH='/Users/mat3ra/Downloads/wave-visualization (7).gif'\n", + "NEW_SIZE_WIDTH=600\n", + "NEW_SIZE_HEIGHT=600\n", + "\n", + "LOGO_PATH= \"/Users/mat3ra/Library/CloudStorage/GoogleDrive-vsevolod.biryukov@exabyte.io/Other computers/My MacBook Pro/marketing/Transparent white_color.png\"\n", + "LOGO_POSITION = (15,10)\n", + "\n", + "FONT_SIZE=24\n", + "TEXT_1=\"Material ABC\"\n", + "TEXT_1_POSITION_X=10\n", + "TEXT_1_POSITION_Y=NEW_SIZE_HEIGHT-20 - FONT_SIZE\n", + "\n", + "FINAL_NAME=\"output.gif\"\n", + "\n", + "gif_processor = GIFProcessor(GIF_PATH)\n", + "\n", + "# # Display original\n", + "# print(\"Original GIF:\")\n", + "# gif_processor.display()\n", + "\n", + "# Resize to 300x200\n", + "# gif_processor.resize(*NEW_SIZE)\n", + "gif_processor.make_square(size=NEW_SIZE_WIDTH)\n", + "\n", + "font_manager = FontManager()\n", + "\n", + "# List available fonts\n", + "print(\"Available fonts:\")\n", + "print(font_manager.list_fonts())\n", + "\n", + "font = \"serif\"\n", + "# Add text overlay\n", + "gif_processor.add_text(\n", + " TEXT_1,\n", + " position=(TEXT_1_POSITION_X, TEXT_1_POSITION_Y),\n", + " font_path=font,\n", + " font_size=FONT_SIZE,\n", + " color=(255, 255, 255),\n", + " stroke_width=2\n", + ")\n", + "\n", + "# Add image overlay (e.g., a logo)\n", + "gif_processor.add_image_overlay(\n", + " LOGO_PATH,\n", + " position=LOGO_POSITION\n", + ")\n", + "\n", + "QUALITY=100\n", + "# Display modified GIF\n", + "print(\"\\nModified GIF:\")\n", + "print(f\"Size: {gif_processor.get_size(quality=QUALITY) / 1024 / 1024:.2f} MB\")\n", + "gif_processor.display()\n", + "\n", + "# Save with compression\n", + "gif_processor.save(\n", + " FINAL_NAME,\n", + " optimize=True,\n", + " quality=QUALITY\n", + ")\n", + "\n" + ], + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": "", + "outputs": [], + "execution_count": null + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "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.9.6" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} From eeb69ce1f6a8f8198f076f2dd0f0b8919486ee8f Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Thu, 9 Jan 2025 21:14:29 -0800 Subject: [PATCH 02/27] update: move gif creation nb --- .../gif_creation.ipynb} | 264 +++++++++++------- 1 file changed, 164 insertions(+), 100 deletions(-) rename other/{materials_designer/wave_js_research.ipynb => media_generation/gif_creation.ipynb} (61%) diff --git a/other/materials_designer/wave_js_research.ipynb b/other/media_generation/gif_creation.ipynb similarity index 61% rename from other/materials_designer/wave_js_research.ipynb rename to other/media_generation/gif_creation.ipynb index 2bdc23d8a..77b2b51b1 100644 --- a/other/materials_designer/wave_js_research.ipynb +++ b/other/media_generation/gif_creation.ipynb @@ -10,13 +10,17 @@ "1. Resize GIFs to specified dimensions\n", "2. Add text overlay at selected positions\n", "3. Add image overlay at selected positions\n", - "4. Compress the resulting GIF" + "4. Compress the resulting GIF\n", + "\n", + "## 1. Import of Libraries." ] }, { "cell_type": "code", "metadata": {}, "source": [ + "import re\n", + "\n", "# Import required libraries\n", "from PIL import Image, ImageDraw, ImageFont\n", "import os\n", @@ -28,51 +32,129 @@ }, { "metadata": {}, + "cell_type": "markdown", + "source": "## 2. Input/Output Settings" + }, + { "cell_type": "code", + "metadata": {}, + "source": [ + "INPUT_DIR = \"input\"\n", + "OUTPUT_DIR = \"output\"\n", + "ASSETS_DIR = \"assets\"\n", + "\n", + "# Create directories if they don't exist\n", + "for directory in [INPUT_DIR, OUTPUT_DIR, ASSETS_DIR]:\n", + " os.makedirs(directory, exist_ok=True)\n", + "\n", + "# GIF Settings\n", + "INPUT_GIF = \"Silicon FCC.gif\" # Place this in files/input/\n", + "OUTPUT_GIF = \"output.gif\" # Will be saved in files/output/\n", + "GIF_SIZE = 600 # Size for square output\n", + "QUALITY = 10 # GIF quality (1-100)\n", + "\n", + "# Logo Settings\n", + "LOGO_FILE = \"logo.png\" # Place your logo in files/assets/\n", + "LOGO_POSITION = (15, 10)\n", + "\n", + "# Text Overlay Settings\n", + "FONT_SIZE = 16\n", + "TEXT_OVERLAYS = [\n", + " {\n", + " \"text\": \"Material ABC\",\n", + " \"position\": (10, GIF_SIZE - 10 - FONT_SIZE), # Bottom left\n", + " \"font\": \"lucida-grande\", # Will fallback to default if not found\n", + " \"color\": (255, 255, 255), # White\n", + " \"stroke_width\": 2,\n", + " \"stroke_fill\": (0, 0, 0) # Black outline\n", + " },\n", + " {\n", + " \"text\": \"Available in our materials bank\",\n", + " \"position\": (GIF_SIZE//2 + 50, GIF_SIZE - 10 - FONT_SIZE), # Bottom right\n", + " \"font\": \"lucida-grande\",\n", + " \"color\": (255, 255, 255),\n", + " \"stroke_width\": 2,\n", + " \"stroke_fill\": (0, 0, 0)\n", + " }\n", + "]\n" + ], + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## 3. Font Manager class" + }, + { + "cell_type": "code", + "metadata": {}, "source": [ "import matplotlib.font_manager as fm\n", "class FontManager:\n", " \"\"\"Manages fonts for the GIF processor\"\"\"\n", "\n", " def __init__(self):\n", - " self.fonts = {\n", - " 'default': None, # Will be initialized with Ubuntu font\n", - " 'matplotlib': {} # Will be populated with matplotlib fonts\n", - " }\n", - " self._initialize_fonts()\n", - "\n", - " def _initialize_fonts(self):\n", - " # Load built-in Ubuntu font\n", - " # Load matplotlib fonts\n", - " font_names = ['serif', 'sans-serif', 'monospace']\n", - " for name in font_names:\n", + " \"\"\"Initialize font manager and discover available fonts\"\"\"\n", + " self.fonts = self._discover_fonts()\n", + "\n", + " def _discover_fonts(self):\n", + " \"\"\"Discover all available fonts in the system\"\"\"\n", + " fonts = {}\n", + "\n", + " # Get all font paths from matplotlib font manager\n", + " for font in fm.fontManager.ttflist:\n", " try:\n", - " font_path = fm.findfont(fm.FontProperties(family=name))\n", - " self.fonts['matplotlib'][name] = font_path\n", - " except:\n", + " # Create a normalized name (lowercase, no spaces)\n", + " name = font.name.lower().replace(' ', '-')\n", + " # Store the font path\n", + " fonts[name] = font.fname\n", + " except Exception as e:\n", " continue\n", "\n", + " return fonts\n", + "\n", " def get_font(self, font_name='default', size=30):\n", " \"\"\"Get a font by name and size\"\"\"\n", " try:\n", + " # Handle default font\n", " if font_name == 'default':\n", - " return ImageFont.truetype(self.fonts['default'], size)\n", - " elif font_name in self.fonts['matplotlib']:\n", - " return ImageFont.truetype(self.fonts['matplotlib'][font_name], size)\n", - " else:\n", - " raise ValueError(f\"Font '{font_name}' not found\")\n", + " return ImageFont.load_default()\n", + "\n", + " # Try exact match\n", + " if font_name in self.fonts:\n", + " return ImageFont.truetype(self.fonts[font_name], size)\n", + "\n", + " # Try fuzzy match (e.g., \"arial-bold\" matches \"arial\")\n", + " fuzzy_matches = [path for name, path in self.fonts.items()\n", + " if font_name in name and name != 'default']\n", + " if fuzzy_matches:\n", + " return ImageFont.truetype(fuzzy_matches[0], size)\n", + "\n", + " raise ValueError(f\"Font '{font_name}' not found\")\n", + "\n", " except Exception as e:\n", " print(f\"Error loading font {font_name}: {str(e)}\")\n", " return ImageFont.load_default()\n", "\n", " def list_fonts(self):\n", " \"\"\"List all available fonts\"\"\"\n", - " available_fonts = ['default'] + list(self.fonts['matplotlib'].keys())\n", - " return available_fonts" + " return ['default'] + sorted(list(self.fonts.keys()))\n", + "\n", + " def search_fonts(self, query):\n", + " \"\"\"Search for fonts containing the query string\"\"\"\n", + " query = query.lower()\n", + " matches = [name for name in self.fonts.keys() if query in name]\n", + " return sorted(matches)" ], "outputs": [], "execution_count": null }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## 4. GIF Processor Class" + }, { "cell_type": "code", "metadata": {}, @@ -163,6 +245,28 @@ " self.frames[i] = frame_rgba\n", " return self\n", "\n", + " def optimize(self, quality=100):\n", + " if not self.frames:\n", + " return\n", + "\n", + " # Convert frames to RGB mode for saving\n", + " rgb_frames = []\n", + " for frame in self.frames:\n", + " rgb_frame = frame.convert('RGB')\n", + " # Calculate number of colors based on quality\n", + " n_colors = max(min(256, int(256 * (quality / 100))), 2)\n", + "\n", + " # Convert to P mode (palette) with optimized palette\n", + " rgb_frame = rgb_frame.quantize(\n", + " colors=n_colors,\n", + " method=Image.Quantize.MEDIANCUT,\n", + " dither=Image.Dither.FLOYDSTEINBERG\n", + " )\n", + " rgb_frames.append(rgb_frame)\n", + "\n", + " self.frames = rgb_frames\n", + "\n", + "\n", " def save(self, output_path, optimize=False, quality=100):\n", " \"\"\"\n", " Save the processed GIF with optimization options\n", @@ -175,24 +279,9 @@ " \"\"\"\n", " if not self.frames:\n", " return\n", - "\n", - " # Convert frames to RGB mode for saving\n", - " rgb_frames = []\n", - " for frame in self.frames:\n", - " rgb_frame = frame.convert('RGB')\n", - "\n", - " if optimize:\n", - " # Calculate number of colors based on quality\n", - " n_colors = max(min(256, int(256 * (quality / 100))), 2)\n", - "\n", - " # Convert to P mode (palette) with optimized palette\n", - " rgb_frame = rgb_frame.quantize(\n", - " colors=n_colors,\n", - " method=Image.Quantize.MEDIANCUT,\n", - " dither=Image.Dither.FLOYDSTEINBERG\n", - " )\n", - "\n", - " rgb_frames.append(rgb_frame)\n", + " if optimize:\n", + " self.optimize(quality)\n", + " rgb_frames = [frame.convert('RGB') for frame in self.frames]\n", "\n", " # Save with optimization\n", " rgb_frames[0].save(\n", @@ -240,85 +329,60 @@ { "cell_type": "markdown", "metadata": {}, - "source": [ - "## Usage Example" - ] + "source": "## 5. Create GIF" }, { "cell_type": "code", "metadata": {}, "source": [ - "# Example usage\n", - "GIF_PATH='/Users/mat3ra/Downloads/wave-visualization (7).gif'\n", - "NEW_SIZE_WIDTH=600\n", - "NEW_SIZE_HEIGHT=600\n", - "\n", - "LOGO_PATH= \"/Users/mat3ra/Library/CloudStorage/GoogleDrive-vsevolod.biryukov@exabyte.io/Other computers/My MacBook Pro/marketing/Transparent white_color.png\"\n", - "LOGO_POSITION = (15,10)\n", - "\n", - "FONT_SIZE=24\n", - "TEXT_1=\"Material ABC\"\n", - "TEXT_1_POSITION_X=10\n", - "TEXT_1_POSITION_Y=NEW_SIZE_HEIGHT-20 - FONT_SIZE\n", - "\n", - "FINAL_NAME=\"output.gif\"\n", - "\n", - "gif_processor = GIFProcessor(GIF_PATH)\n", - "\n", - "# # Display original\n", - "# print(\"Original GIF:\")\n", - "# gif_processor.display()\n", - "\n", - "# Resize to 300x200\n", - "# gif_processor.resize(*NEW_SIZE)\n", - "gif_processor.make_square(size=NEW_SIZE_WIDTH)\n", + "input_gif_path = os.path.join(INPUT_DIR, INPUT_GIF)\n", + "output_gif_path = os.path.join(OUTPUT_DIR, OUTPUT_GIF)\n", + "logo_path = os.path.join(ASSETS_DIR, LOGO_FILE)\n", "\n", + "# Initialize font manager and list available fonts\n", "font_manager = FontManager()\n", - "\n", - "# List available fonts\n", "print(\"Available fonts:\")\n", "print(font_manager.list_fonts())\n", "\n", - "font = \"serif\"\n", - "# Add text overlay\n", - "gif_processor.add_text(\n", - " TEXT_1,\n", - " position=(TEXT_1_POSITION_X, TEXT_1_POSITION_Y),\n", - " font_path=font,\n", - " font_size=FONT_SIZE,\n", - " color=(255, 255, 255),\n", - " stroke_width=2\n", - ")\n", - "\n", - "# Add image overlay (e.g., a logo)\n", - "gif_processor.add_image_overlay(\n", - " LOGO_PATH,\n", - " position=LOGO_POSITION\n", - ")\n", - "\n", - "QUALITY=100\n", - "# Display modified GIF\n", + "# Process the GIF\n", + "gif_processor = GIFProcessor(input_gif_path)\n", + "gif_processor.make_square(size=GIF_SIZE)\n", + "\n", + "# Add text overlays\n", + "for overlay in TEXT_OVERLAYS:\n", + " gif_processor.add_text(\n", + " text=overlay[\"text\"],\n", + " position=overlay[\"position\"],\n", + " font_path=overlay[\"font\"],\n", + " font_size=FONT_SIZE,\n", + " color=overlay[\"color\"],\n", + " stroke_width=overlay[\"stroke_width\"],\n", + " stroke_fill=overlay[\"stroke_fill\"]\n", + " )\n", + "\n", + "# Add logo overlay\n", + "if os.path.exists(logo_path):\n", + " gif_processor.add_image_overlay(\n", + " logo_path,\n", + " position=LOGO_POSITION\n", + " )\n", + "else:\n", + " print(f\"Warning: Logo file not found at {logo_path}\")\n", + "\n", + "# Optimize and display\n", + "gif_processor.optimize(quality=QUALITY)\n", "print(\"\\nModified GIF:\")\n", - "print(f\"Size: {gif_processor.get_size(quality=QUALITY) / 1024 / 1024:.2f} MB\")\n", "gif_processor.display()\n", "\n", "# Save with compression\n", "gif_processor.save(\n", - " FINAL_NAME,\n", - " optimize=True,\n", + " output_gif_path,\n", + " optimize=False,\n", " quality=QUALITY\n", - ")\n", - "\n" + ")" ], "outputs": [], "execution_count": null - }, - { - "metadata": {}, - "cell_type": "code", - "source": "", - "outputs": [], - "execution_count": null } ], "metadata": { From a678e9b18c86e071804b6bf281e41377525ca163 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Thu, 9 Jan 2025 21:22:48 -0800 Subject: [PATCH 03/27] update: optimize --- other/media_generation/gif_creation.ipynb | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/other/media_generation/gif_creation.ipynb b/other/media_generation/gif_creation.ipynb index 77b2b51b1..5b807da7f 100644 --- a/other/media_generation/gif_creation.ipynb +++ b/other/media_generation/gif_creation.ipynb @@ -59,22 +59,25 @@ "\n", "# Text Overlay Settings\n", "FONT_SIZE = 16\n", + "WHITE_COLOR = (255, 255, 255)\n", + "BLACK_COLOR = (0, 0, 0)\n", + "FONT_NAME = \"lucida-grande\"\n", "TEXT_OVERLAYS = [\n", " {\n", " \"text\": \"Material ABC\",\n", " \"position\": (10, GIF_SIZE - 10 - FONT_SIZE), # Bottom left\n", - " \"font\": \"lucida-grande\", # Will fallback to default if not found\n", - " \"color\": (255, 255, 255), # White\n", + " \"font\": FONT_NAME,\n", + " \"color\": WHITE_COLOR,\n", " \"stroke_width\": 2,\n", - " \"stroke_fill\": (0, 0, 0) # Black outline\n", + " \"stroke_fill\": BLACK_COLOR\n", " },\n", " {\n", " \"text\": \"Available in our materials bank\",\n", " \"position\": (GIF_SIZE//2 + 50, GIF_SIZE - 10 - FONT_SIZE), # Bottom right\n", - " \"font\": \"lucida-grande\",\n", - " \"color\": (255, 255, 255),\n", + " \"font\": FONT_NAME,\n", + " \"color\": WHITE_COLOR,\n", " \"stroke_width\": 2,\n", - " \"stroke_fill\": (0, 0, 0)\n", + " \"stroke_fill\": BLACK_COLOR\n", " }\n", "]\n" ], From 6e4f767f0e0d913e3dab78f536cab3b14169e2d9 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 10 Jan 2025 09:38:33 -0800 Subject: [PATCH 04/27] update: restructure --- other/media_generation/gif_creation.ipynb | 26 +++++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/other/media_generation/gif_creation.ipynb b/other/media_generation/gif_creation.ipynb index 5b807da7f..a3595f6f7 100644 --- a/other/media_generation/gif_creation.ipynb +++ b/other/media_generation/gif_creation.ipynb @@ -48,10 +48,10 @@ " os.makedirs(directory, exist_ok=True)\n", "\n", "# GIF Settings\n", - "INPUT_GIF = \"Silicon FCC.gif\" # Place this in files/input/\n", + "INPUT_GIF = \"MoS2_O4V2.gif\" # Place this in files/input/\n", "OUTPUT_GIF = \"output.gif\" # Will be saved in files/output/\n", "GIF_SIZE = 600 # Size for square output\n", - "QUALITY = 10 # GIF quality (1-100)\n", + "QUALITY = 20 # GIF quality (1-100)\n", "\n", "# Logo Settings\n", "LOGO_FILE = \"logo.png\" # Place your logo in files/assets/\n", @@ -153,6 +153,23 @@ "outputs": [], "execution_count": null }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## 3.2. List fonts" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Initialize font manager and list available fonts\n", + "font_manager = FontManager()\n", + "print(\"Available fonts:\")\n", + "print(font_manager.list_fonts())" + ], + "outputs": [], + "execution_count": null + }, { "metadata": {}, "cell_type": "markdown", @@ -342,11 +359,6 @@ "output_gif_path = os.path.join(OUTPUT_DIR, OUTPUT_GIF)\n", "logo_path = os.path.join(ASSETS_DIR, LOGO_FILE)\n", "\n", - "# Initialize font manager and list available fonts\n", - "font_manager = FontManager()\n", - "print(\"Available fonts:\")\n", - "print(font_manager.list_fonts())\n", - "\n", "# Process the GIF\n", "gif_processor = GIFProcessor(input_gif_path)\n", "gif_processor.make_square(size=GIF_SIZE)\n", From f54f463fb841e3fb47e87fe4d92cd8f7784ac61f Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Wed, 15 Jan 2025 15:02:33 -0800 Subject: [PATCH 05/27] update: control wave with messages --- other/media_generation/wave_control.ipynb | 288 ++++++++++++++++++++++ 1 file changed, 288 insertions(+) create mode 100644 other/media_generation/wave_control.ipynb diff --git a/other/media_generation/wave_control.ipynb b/other/media_generation/wave_control.ipynb new file mode 100644 index 000000000..f917f14f8 --- /dev/null +++ b/other/media_generation/wave_control.ipynb @@ -0,0 +1,288 @@ +{ + "cells": [ + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-15T23:00:07.177049Z", + "start_time": "2025-01-15T23:00:07.170608Z" + } + }, + "cell_type": "code", + "source": [ + "from IPython.display import display, IFrame, Javascript\n", + "import json\n", + "\n", + "from utils.visualize import visualize_materials\n", + "\n", + "\n", + "def send_message_to_iframe(message):\n", + " \"\"\"\n", + " Creates and displays an iframe pointing to `url`,\n", + " then sends `message` to the iframe via postMessage.\n", + " \"\"\"\n", + " # Create and display iframe\n", + "\n", + "\n", + " # Prepare JavaScript code to send message to iframe\n", + " js_code = f\"\"\"\n", + " (function() {{\n", + " // Wait for the iframe to be available\n", + " const iframe = document.querySelector('iframe');\n", + " if (iframe && iframe.contentWindow) {{\n", + " setTimeout(() => {{\n", + " try {{\n", + " iframe.contentWindow.postMessage({json.dumps(message)}, '*');\n", + " console.log('Message sent to iframe');\n", + " }} catch (error) {{\n", + " console.error('Error sending message:', error);\n", + " }}\n", + " }}, 1000); // slight delay to ensure iframe is ready\n", + " }} else {{\n", + " console.error('Iframe not found or not ready');\n", + " }}\n", + " }})();\n", + " \"\"\"\n", + " display(Javascript(js_code))\n", + "\n", + "\n", + "def set_material_in_iframe(material_json):\n", + " \"\"\"\n", + " Uses send_message_to_iframe to send a material configuration\n", + " under the \"material\" key to the iframe at `url`.\n", + " \"\"\"\n", + " send_message_to_iframe({\"material\": material_json})\n", + "\n", + "\n", + "def record_gif(filename, rotation_speed=60, frame_duration=0.05):\n", + " \"\"\"\n", + " Uses send_message_to_iframe to send a message to the iframe at `url`\n", + " to start recording a GIF of the visualization with the specified parameters.\n", + " \"\"\"\n", + " message = {\n", + " \"action\": \"handleStartGifRecording\",\n", + " \"parameters\": [filename, rotation_speed, frame_duration]\n", + " }\n", + " send_message_to_iframe(message)\n", + "\n", + "def set_camera( pos=(0, 5, 0), target=(0, 0, 0)):\n", + " func_str = f\"(wave) => {{ wave.camera.position.set({pos[0]}, {pos[1]}, {pos[2]}); wave.camera.lookAt({target[0]}, {target[1]}, {target[2]}); wave.render(); }}\"\n", + " message = {\n", + " \"action\": \"doFunc\",\n", + " \"parameters\": [func_str]\n", + " }\n", + " send_message_to_iframe(message)\n", + "\n", + "def toggle_bonds():\n", + " message = {\n", + " \"action\": \"handleToggleBonds\",\n", + " \"parameters\": [None]\n", + " }\n", + " send_message_to_iframe(message)\n", + "\n" + ], + "id": "93be5f1320e953c6", + "outputs": [], + "execution_count": 13 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-15T23:00:07.752017Z", + "start_time": "2025-01-15T23:00:07.739055Z" + } + }, + "cell_type": "code", + "source": [ + "# Example usage with a material configuration:\n", + "import time\n", + "material_example = {\n", + " \"name\": \"New Material\",\n", + " \"basis\": {\n", + " \"elements\": [\n", + " {\"id\": 0, \"value\": \"O\"},\n", + " {\"id\": 1, \"value\": \"C\"}\n", + " ],\n", + " \"coordinates\": [\n", + " {\"id\": 0, \"value\": [0, 0, 0]},\n", + " {\"id\": 1, \"value\": [0.25, 0.25, 0.5]}\n", + " ],\n", + " \"units\": \"crystal\",\n", + " \"cell\": [\n", + " [3.34892, 0, 1.9335],\n", + " [1.116307, 3.157392, 1.9335],\n", + " [0, 0, 3.867]\n", + " ],\n", + " \"constraints\": []\n", + " },\n", + " \"lattice\": {\n", + " \"a\": 3.867, \"b\": 3.867, \"c\": 3.867,\n", + " \"alpha\": 60, \"beta\": 90, \"gamma\": 90,\n", + " \"units\": {\"length\": \"angstrom\", \"angle\": \"degree\"},\n", + " \"type\": \"FCC\",\n", + " \"vectors\": {\n", + " \"a\": [3.34892, 0, 1.9335],\n", + " \"b\": [1.116307, 3.157392, 1.9335],\n", + " \"c\": [0, 0, 3.867],\n", + " \"alat\": 1,\n", + " \"units\": \"angstrom\"\n", + " }\n", + " },\n", + " \"isNonPeriodic\": False\n", + "}\n", + "\n", + "URL = \"http://localhost:3002\"\n", + "GIF_NAME = \"test_gif.gif\"\n", + "\n", + "iframe = IFrame(src=URL, width=\"800\", height=\"600\")\n", + "display(iframe)\n", + "# Send the material configuration to the iframe at the specified URL\n", + "set_material_in_iframe(material_example)\n", + "toggle_bonds()\n", + "set_camera()\n", + "record_gif( GIF_NAME, 60, 0.05)\n" + ], + "id": "6f65f2a33a405f16", + "outputs": [ + { + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + " \n", + " " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "" + ], + "application/javascript": "\n (function() {\n // Wait for the iframe to be available\n const iframe = document.querySelector('iframe');\n if (iframe && iframe.contentWindow) {\n setTimeout(() => {\n try {\n iframe.contentWindow.postMessage({\"material\": {\"name\": \"New Material\", \"basis\": {\"elements\": [{\"id\": 0, \"value\": \"O\"}, {\"id\": 1, \"value\": \"C\"}], \"coordinates\": [{\"id\": 0, \"value\": [0, 0, 0]}, {\"id\": 1, \"value\": [0.25, 0.25, 0.5]}], \"units\": \"crystal\", \"cell\": [[3.34892, 0, 1.9335], [1.116307, 3.157392, 1.9335], [0, 0, 3.867]], \"constraints\": []}, \"lattice\": {\"a\": 3.867, \"b\": 3.867, \"c\": 3.867, \"alpha\": 60, \"beta\": 90, \"gamma\": 90, \"units\": {\"length\": \"angstrom\", \"angle\": \"degree\"}, \"type\": \"FCC\", \"vectors\": {\"a\": [3.34892, 0, 1.9335], \"b\": [1.116307, 3.157392, 1.9335], \"c\": [0, 0, 3.867], \"alat\": 1, \"units\": \"angstrom\"}}, \"isNonPeriodic\": false}}, '*');\n console.log('Message sent to iframe');\n } catch (error) {\n console.error('Error sending message:', error);\n }\n }, 1000); // slight delay to ensure iframe is ready\n } else {\n console.error('Iframe not found or not ready');\n }\n })();\n " + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "" + ], + "application/javascript": "\n (function() {\n // Wait for the iframe to be available\n const iframe = document.querySelector('iframe');\n if (iframe && iframe.contentWindow) {\n setTimeout(() => {\n try {\n iframe.contentWindow.postMessage({\"action\": \"handleToggleBonds\", \"parameters\": [null]}, '*');\n console.log('Message sent to iframe');\n } catch (error) {\n console.error('Error sending message:', error);\n }\n }, 1000); // slight delay to ensure iframe is ready\n } else {\n console.error('Iframe not found or not ready');\n }\n })();\n " + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "" + ], + "application/javascript": "\n (function() {\n // Wait for the iframe to be available\n const iframe = document.querySelector('iframe');\n if (iframe && iframe.contentWindow) {\n setTimeout(() => {\n try {\n iframe.contentWindow.postMessage({\"action\": \"doWaveFunc\", \"parameters\": [\"(wave) => {\\n // Get the current camera\\n const camera = wave.camera;\\n // Set position and look target\\n camera.position.set(0, 0, 1);\\n camera.lookAt(0, 0, 0);\\n // Update orbit controls if they exist\\n if (wave.orbitControls) {\\n wave.orbitControls.target.set(0, 0, 0);\\n wave.orbitControls.update();\\n }\\n // Force a render\\n wave.render();\\n }\"]}, '*');\n console.log('Message sent to iframe');\n } catch (error) {\n console.error('Error sending message:', error);\n }\n }, 1000); // slight delay to ensure iframe is ready\n } else {\n console.error('Iframe not found or not ready');\n }\n })();\n " + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "" + ], + "application/javascript": "\n (function() {\n // Wait for the iframe to be available\n const iframe = document.querySelector('iframe');\n if (iframe && iframe.contentWindow) {\n setTimeout(() => {\n try {\n iframe.contentWindow.postMessage({\"action\": \"handleStartGifRecording\", \"parameters\": [\"test_gif.gif\", 60, 0.05]}, '*');\n console.log('Message sent to iframe');\n } catch (error) {\n console.error('Error sending message:', error);\n }\n }, 1000); // slight delay to ensure iframe is ready\n } else {\n console.error('Iframe not found or not ready');\n }\n })();\n " + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "execution_count": 14 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-15T22:54:48.999261Z", + "start_time": "2025-01-15T22:54:48.856606Z" + } + }, + "cell_type": "code", + "source": [ + "# get current path\n", + "import os\n", + "\n", + "from IPython.display import IFrame, Javascript\n", + "import json\n", + "\n", + "url = \"http://localhost:3002\"\n", + "\n", + "\n", + "work_dir = os.getcwd()\n", + "\n", + "name = \"generic_command_test.gif\"\n", + "full_name = os.path.join(work_dir, name)\n", + "\n", + "# Message to trigger handleStartGifRecording\n", + "message = {\n", + " \"action\": \"handleStartGifRecording\",\n", + " \"parameters\": [full_name, 60, 0.05] # [downloadPath, rotationSpeed, frameDuration]\n", + "}\n", + "\n", + "# Send the message to the iframe\n", + "send_message_to_iframe(url, message)\n" + ], + "id": "af3732c1752b3ab8", + "outputs": [ + { + "ename": "TypeError", + "evalue": "send_message_to_iframe() takes 1 positional argument but 2 were given", + "output_type": "error", + "traceback": [ + "\u001B[0;31m---------------------------------------------------------------------------\u001B[0m", + "\u001B[0;31mTypeError\u001B[0m Traceback (most recent call last)", + "Cell \u001B[0;32mIn[3], line 22\u001B[0m\n\u001B[1;32m 16\u001B[0m message \u001B[38;5;241m=\u001B[39m {\n\u001B[1;32m 17\u001B[0m \u001B[38;5;124m\"\u001B[39m\u001B[38;5;124maction\u001B[39m\u001B[38;5;124m\"\u001B[39m: \u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mhandleStartGifRecording\u001B[39m\u001B[38;5;124m\"\u001B[39m,\n\u001B[1;32m 18\u001B[0m \u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mparameters\u001B[39m\u001B[38;5;124m\"\u001B[39m: [full_name, \u001B[38;5;241m60\u001B[39m, \u001B[38;5;241m0.05\u001B[39m] \u001B[38;5;66;03m# [downloadPath, rotationSpeed, frameDuration]\u001B[39;00m\n\u001B[1;32m 19\u001B[0m }\n\u001B[1;32m 21\u001B[0m \u001B[38;5;66;03m# Send the message to the iframe\u001B[39;00m\n\u001B[0;32m---> 22\u001B[0m \u001B[43msend_message_to_iframe\u001B[49m\u001B[43m(\u001B[49m\u001B[43murl\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mmessage\u001B[49m\u001B[43m)\u001B[49m\n", + "\u001B[0;31mTypeError\u001B[0m: send_message_to_iframe() takes 1 positional argument but 2 were given" + ] + } + ], + "execution_count": 3 + }, + { + "metadata": {}, + "cell_type": "code", + "source": "", + "id": "bd1e9c79e5457f3b", + "outputs": [], + "execution_count": null + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv-3.11.2", + "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.11.2" + } + }, + "nbformat": 5, + "nbformat_minor": 9 +} From f3cd7aa2b98b75e5009fe92a5ba872c8f11dc0db Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Wed, 15 Jan 2025 15:08:21 -0800 Subject: [PATCH 06/27] update: cleanup and strucutre --- other/media_generation/wave_control.ipynb | 265 ++++++++-------------- 1 file changed, 94 insertions(+), 171 deletions(-) diff --git a/other/media_generation/wave_control.ipynb b/other/media_generation/wave_control.ipynb index f917f14f8..1bfcac834 100644 --- a/other/media_generation/wave_control.ipynb +++ b/other/media_generation/wave_control.ipynb @@ -1,12 +1,81 @@ { "cells": [ { - "metadata": { - "ExecuteTime": { - "end_time": "2025-01-15T23:00:07.177049Z", - "start_time": "2025-01-15T23:00:07.170608Z" - } - }, + "metadata": {}, + "cell_type": "markdown", + "source": "## 1. Settings of Notebook", + "id": "5542df901e7c76cc" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": "URL = \"http://localhost:3002\"\n", + "id": "79ee60afd06b7b3f" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## 2. Loading of materials", + "id": "a10c370c09229a6b" + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": [ + "material_json = {\n", + " \"name\": \"New Material\",\n", + " \"basis\": {\n", + " \"elements\": [\n", + " {\"id\": 0, \"value\": \"O\"},\n", + " {\"id\": 1, \"value\": \"C\"}\n", + " ],\n", + " \"coordinates\": [\n", + " {\"id\": 0, \"value\": [0, 0, 0]},\n", + " {\"id\": 1, \"value\": [0.25, 0.25, 0.5]}\n", + " ],\n", + " \"units\": \"crystal\",\n", + " \"cell\": [\n", + " [3.34892, 0, 1.9335],\n", + " [1.116307, 3.157392, 1.9335],\n", + " [0, 0, 3.867]\n", + " ],\n", + " \"constraints\": []\n", + " },\n", + " \"lattice\": {\n", + " \"a\": 3.867, \"b\": 3.867, \"c\": 3.867,\n", + " \"alpha\": 60, \"beta\": 90, \"gamma\": 90,\n", + " \"units\": {\"length\": \"angstrom\", \"angle\": \"degree\"},\n", + " \"type\": \"FCC\",\n", + " \"vectors\": {\n", + " \"a\": [3.34892, 0, 1.9335],\n", + " \"b\": [1.116307, 3.157392, 1.9335],\n", + " \"c\": [0, 0, 3.867],\n", + " \"alat\": 1,\n", + " \"units\": \"angstrom\"\n", + " }\n", + " },\n", + " \"isNonPeriodic\": False\n", + "}\n", + "\n", + "materials_settings = [{\n", + " \"material_json\": material_json,\n", + " \"name\": \"New Material.gif\"\n", + "}]" + ], + "id": "191df844bfc61a4e" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## 3. Definitions", + "id": "384f7d7270ecbf53" + }, + { + "metadata": {}, "cell_type": "code", "source": [ "from IPython.display import display, IFrame, Javascript\n", @@ -82,178 +151,32 @@ ], "id": "93be5f1320e953c6", "outputs": [], - "execution_count": 13 + "execution_count": null }, { - "metadata": { - "ExecuteTime": { - "end_time": "2025-01-15T23:00:07.752017Z", - "start_time": "2025-01-15T23:00:07.739055Z" - } - }, - "cell_type": "code", - "source": [ - "# Example usage with a material configuration:\n", - "import time\n", - "material_example = {\n", - " \"name\": \"New Material\",\n", - " \"basis\": {\n", - " \"elements\": [\n", - " {\"id\": 0, \"value\": \"O\"},\n", - " {\"id\": 1, \"value\": \"C\"}\n", - " ],\n", - " \"coordinates\": [\n", - " {\"id\": 0, \"value\": [0, 0, 0]},\n", - " {\"id\": 1, \"value\": [0.25, 0.25, 0.5]}\n", - " ],\n", - " \"units\": \"crystal\",\n", - " \"cell\": [\n", - " [3.34892, 0, 1.9335],\n", - " [1.116307, 3.157392, 1.9335],\n", - " [0, 0, 3.867]\n", - " ],\n", - " \"constraints\": []\n", - " },\n", - " \"lattice\": {\n", - " \"a\": 3.867, \"b\": 3.867, \"c\": 3.867,\n", - " \"alpha\": 60, \"beta\": 90, \"gamma\": 90,\n", - " \"units\": {\"length\": \"angstrom\", \"angle\": \"degree\"},\n", - " \"type\": \"FCC\",\n", - " \"vectors\": {\n", - " \"a\": [3.34892, 0, 1.9335],\n", - " \"b\": [1.116307, 3.157392, 1.9335],\n", - " \"c\": [0, 0, 3.867],\n", - " \"alat\": 1,\n", - " \"units\": \"angstrom\"\n", - " }\n", - " },\n", - " \"isNonPeriodic\": False\n", - "}\n", - "\n", - "URL = \"http://localhost:3002\"\n", - "GIF_NAME = \"test_gif.gif\"\n", - "\n", - "iframe = IFrame(src=URL, width=\"800\", height=\"600\")\n", - "display(iframe)\n", - "# Send the material configuration to the iframe at the specified URL\n", - "set_material_in_iframe(material_example)\n", - "toggle_bonds()\n", - "set_camera()\n", - "record_gif( GIF_NAME, 60, 0.05)\n" - ], - "id": "6f65f2a33a405f16", - "outputs": [ - { - "data": { - "text/plain": [ - "" - ], - "text/html": [ - "\n", - " \n", - " " - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "" - ], - "application/javascript": "\n (function() {\n // Wait for the iframe to be available\n const iframe = document.querySelector('iframe');\n if (iframe && iframe.contentWindow) {\n setTimeout(() => {\n try {\n iframe.contentWindow.postMessage({\"material\": {\"name\": \"New Material\", \"basis\": {\"elements\": [{\"id\": 0, \"value\": \"O\"}, {\"id\": 1, \"value\": \"C\"}], \"coordinates\": [{\"id\": 0, \"value\": [0, 0, 0]}, {\"id\": 1, \"value\": [0.25, 0.25, 0.5]}], \"units\": \"crystal\", \"cell\": [[3.34892, 0, 1.9335], [1.116307, 3.157392, 1.9335], [0, 0, 3.867]], \"constraints\": []}, \"lattice\": {\"a\": 3.867, \"b\": 3.867, \"c\": 3.867, \"alpha\": 60, \"beta\": 90, \"gamma\": 90, \"units\": {\"length\": \"angstrom\", \"angle\": \"degree\"}, \"type\": \"FCC\", \"vectors\": {\"a\": [3.34892, 0, 1.9335], \"b\": [1.116307, 3.157392, 1.9335], \"c\": [0, 0, 3.867], \"alat\": 1, \"units\": \"angstrom\"}}, \"isNonPeriodic\": false}}, '*');\n console.log('Message sent to iframe');\n } catch (error) {\n console.error('Error sending message:', error);\n }\n }, 1000); // slight delay to ensure iframe is ready\n } else {\n console.error('Iframe not found or not ready');\n }\n })();\n " - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "" - ], - "application/javascript": "\n (function() {\n // Wait for the iframe to be available\n const iframe = document.querySelector('iframe');\n if (iframe && iframe.contentWindow) {\n setTimeout(() => {\n try {\n iframe.contentWindow.postMessage({\"action\": \"handleToggleBonds\", \"parameters\": [null]}, '*');\n console.log('Message sent to iframe');\n } catch (error) {\n console.error('Error sending message:', error);\n }\n }, 1000); // slight delay to ensure iframe is ready\n } else {\n console.error('Iframe not found or not ready');\n }\n })();\n " - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "" - ], - "application/javascript": "\n (function() {\n // Wait for the iframe to be available\n const iframe = document.querySelector('iframe');\n if (iframe && iframe.contentWindow) {\n setTimeout(() => {\n try {\n iframe.contentWindow.postMessage({\"action\": \"doWaveFunc\", \"parameters\": [\"(wave) => {\\n // Get the current camera\\n const camera = wave.camera;\\n // Set position and look target\\n camera.position.set(0, 0, 1);\\n camera.lookAt(0, 0, 0);\\n // Update orbit controls if they exist\\n if (wave.orbitControls) {\\n wave.orbitControls.target.set(0, 0, 0);\\n wave.orbitControls.update();\\n }\\n // Force a render\\n wave.render();\\n }\"]}, '*');\n console.log('Message sent to iframe');\n } catch (error) {\n console.error('Error sending message:', error);\n }\n }, 1000); // slight delay to ensure iframe is ready\n } else {\n console.error('Iframe not found or not ready');\n }\n })();\n " - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "" - ], - "application/javascript": "\n (function() {\n // Wait for the iframe to be available\n const iframe = document.querySelector('iframe');\n if (iframe && iframe.contentWindow) {\n setTimeout(() => {\n try {\n iframe.contentWindow.postMessage({\"action\": \"handleStartGifRecording\", \"parameters\": [\"test_gif.gif\", 60, 0.05]}, '*');\n console.log('Message sent to iframe');\n } catch (error) {\n console.error('Error sending message:', error);\n }\n }, 1000); // slight delay to ensure iframe is ready\n } else {\n console.error('Iframe not found or not ready');\n }\n })();\n " - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "execution_count": 14 + "metadata": {}, + "cell_type": "markdown", + "source": "## 4. Gif Generation", + "id": "592649ebe1e44c46" }, { - "metadata": { - "ExecuteTime": { - "end_time": "2025-01-15T22:54:48.999261Z", - "start_time": "2025-01-15T22:54:48.856606Z" - } - }, + "metadata": {}, "cell_type": "code", "source": [ - "# get current path\n", - "import os\n", - "\n", - "from IPython.display import IFrame, Javascript\n", - "import json\n", - "\n", - "url = \"http://localhost:3002\"\n", - "\n", - "\n", - "work_dir = os.getcwd()\n", - "\n", - "name = \"generic_command_test.gif\"\n", - "full_name = os.path.join(work_dir, name)\n", - "\n", - "# Message to trigger handleStartGifRecording\n", - "message = {\n", - " \"action\": \"handleStartGifRecording\",\n", - " \"parameters\": [full_name, 60, 0.05] # [downloadPath, rotationSpeed, frameDuration]\n", - "}\n", - "\n", - "# Send the message to the iframe\n", - "send_message_to_iframe(url, message)\n" - ], - "id": "af3732c1752b3ab8", - "outputs": [ - { - "ename": "TypeError", - "evalue": "send_message_to_iframe() takes 1 positional argument but 2 were given", - "output_type": "error", - "traceback": [ - "\u001B[0;31m---------------------------------------------------------------------------\u001B[0m", - "\u001B[0;31mTypeError\u001B[0m Traceback (most recent call last)", - "Cell \u001B[0;32mIn[3], line 22\u001B[0m\n\u001B[1;32m 16\u001B[0m message \u001B[38;5;241m=\u001B[39m {\n\u001B[1;32m 17\u001B[0m \u001B[38;5;124m\"\u001B[39m\u001B[38;5;124maction\u001B[39m\u001B[38;5;124m\"\u001B[39m: \u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mhandleStartGifRecording\u001B[39m\u001B[38;5;124m\"\u001B[39m,\n\u001B[1;32m 18\u001B[0m \u001B[38;5;124m\"\u001B[39m\u001B[38;5;124mparameters\u001B[39m\u001B[38;5;124m\"\u001B[39m: [full_name, \u001B[38;5;241m60\u001B[39m, \u001B[38;5;241m0.05\u001B[39m] \u001B[38;5;66;03m# [downloadPath, rotationSpeed, frameDuration]\u001B[39;00m\n\u001B[1;32m 19\u001B[0m }\n\u001B[1;32m 21\u001B[0m \u001B[38;5;66;03m# Send the message to the iframe\u001B[39;00m\n\u001B[0;32m---> 22\u001B[0m \u001B[43msend_message_to_iframe\u001B[49m\u001B[43m(\u001B[49m\u001B[43murl\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mmessage\u001B[49m\u001B[43m)\u001B[49m\n", - "\u001B[0;31mTypeError\u001B[0m: send_message_to_iframe() takes 1 positional argument but 2 were given" - ] - } + "# Example usage with a material configuration:\n", + "for material_settings in materials_settings:\n", + " GIF_NAME = material_settings[\"name\"] or material_settings[\"material_json\"][\"name\"]\n", + " iframe = IFrame(src=URL, width=\"800\", height=\"600\")\n", + " display(iframe)\n", + " # Send the material configuration to the iframe at the specified URL\n", + " set_material_in_iframe(material_json)\n", + " toggle_bonds()\n", + " set_camera()\n", + " record_gif( GIF_NAME, 60, 0.05)\n" ], - "execution_count": 3 + "id": "6f65f2a33a405f16", + "outputs": [], + "execution_count": null }, { "metadata": {}, From d465ba69b5471869e8162144c59b233f5f4f5ecd Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Wed, 15 Jan 2025 18:02:07 -0800 Subject: [PATCH 07/27] update: wait for gif to generate, reset viewer --- other/media_generation/wave_control.ipynb | 124 ++++++++++++++++++---- 1 file changed, 104 insertions(+), 20 deletions(-) diff --git a/other/media_generation/wave_control.ipynb b/other/media_generation/wave_control.ipynb index 1bfcac834..772f4a729 100644 --- a/other/media_generation/wave_control.ipynb +++ b/other/media_generation/wave_control.ipynb @@ -9,10 +9,14 @@ { "metadata": {}, "cell_type": "code", + "source": [ + "import time\n", + "\n", + "URL = \"http://localhost:3002\"\n" + ], + "id": "79ee60afd06b7b3f", "outputs": [], - "execution_count": null, - "source": "URL = \"http://localhost:3002\"\n", - "id": "79ee60afd06b7b3f" + "execution_count": null }, { "metadata": {}, @@ -23,19 +27,17 @@ { "metadata": {}, "cell_type": "code", - "outputs": [], - "execution_count": null, "source": [ - "material_json = {\n", + "material_json_1 = {\n", " \"name\": \"New Material\",\n", " \"basis\": {\n", " \"elements\": [\n", - " {\"id\": 0, \"value\": \"O\"},\n", - " {\"id\": 1, \"value\": \"C\"}\n", + " {\"id\": 0, \"value\": \"Si\"},\n", + " {\"id\": 1, \"value\": \"Si\"}\n", " ],\n", " \"coordinates\": [\n", " {\"id\": 0, \"value\": [0, 0, 0]},\n", - " {\"id\": 1, \"value\": [0.25, 0.25, 0.5]}\n", + " {\"id\": 1, \"value\": [0.25, 0.25, 0.25]}\n", " ],\n", " \"units\": \"crystal\",\n", " \"cell\": [\n", @@ -61,12 +63,52 @@ " \"isNonPeriodic\": False\n", "}\n", "\n", + "material_json_2 = {\n", + " \"name\": \"Another Material\",\n", + " \"basis\": {\n", + " \"elements\": [\n", + " {\"id\": 0, \"value\": \"Si\"},\n", + " {\"id\": 1, \"value\": \"O\"}\n", + " ],\n", + " \"coordinates\": [\n", + " {\"id\": 0, \"value\": [0, 0, 0]},\n", + " {\"id\": 1, \"value\": [0.15, 0.15, 0.15]}\n", + " ],\n", + " \"units\": \"crystal\",\n", + " \"cell\": [\n", + " [5.0, 0, 0],\n", + " [0, 5.0, 0],\n", + " [0, 0, 5.0]\n", + " ],\n", + " \"constraints\": []\n", + " },\n", + " \"lattice\": {\n", + " \"a\": 5.0, \"b\": 5.0, \"c\": 5.0,\n", + " \"alpha\": 90, \"beta\": 90, \"gamma\": 60,\n", + " \"units\": {\"length\": \"angstrom\", \"angle\": \"degree\"},\n", + " \"type\": \"CUB\",\n", + " \"vectors\": {\n", + " \"a\": [5.0, 0, 0],\n", + " \"b\": [0, 5.0, 0],\n", + " \"c\": [0, 0, 5.0],\n", + " \"alat\": 1,\n", + " \"units\": \"angstrom\"\n", + " }\n", + " },\n", + " \"isNonPeriodic\": False\n", + "}\n", + "\n", "materials_settings = [{\n", - " \"material_json\": material_json,\n", - " \"name\": \"New Material.gif\"\n", + " \"material_json\": material_json_1,\n", + " \"name\": \"New Material 1 (Si2).gif\"\n", + "},\n", + "{\"material_json\": material_json_2,\n", + " \"name\": \"New Material 2 (SiO).gif\"\n", "}]" ], - "id": "191df844bfc61a4e" + "id": "191df844bfc61a4e", + "outputs": [], + "execution_count": null }, { "metadata": {}, @@ -78,11 +120,27 @@ "metadata": {}, "cell_type": "code", "source": [ - "from IPython.display import display, IFrame, Javascript\n", + "from IPython.display import display, IFrame, Javascript, HTML\n", "import json\n", "\n", "from utils.visualize import visualize_materials\n", "\n", + "def reset_iframe():\n", + " \"\"\"\n", + " Resets the iframe content by reloading it.\n", + " \"\"\"\n", + " js_code = \"\"\"\n", + " (function() {\n", + " const iframe = document.querySelector('iframe');\n", + " if (iframe) {\n", + " iframe.src = iframe.src; // Reload the iframe\n", + " console.log('Iframe reset');\n", + " } else {\n", + " console.error('Iframe not found');\n", + " }\n", + " })();\n", + " \"\"\"\n", + " display(Javascript(js_code))\n", "\n", "def send_message_to_iframe(message):\n", " \"\"\"\n", @@ -134,7 +192,7 @@ " send_message_to_iframe(message)\n", "\n", "def set_camera( pos=(0, 5, 0), target=(0, 0, 0)):\n", - " func_str = f\"(wave) => {{ wave.camera.position.set({pos[0]}, {pos[1]}, {pos[2]}); wave.camera.lookAt({target[0]}, {target[1]}, {target[2]}); wave.render(); }}\"\n", + " func_str = f\"(wave) => {{ alert('set_camera'); wave.camera.position.set({pos[0]}, {pos[1]}, {pos[2]}); wave.rebuildScene(); }}\"\n", " message = {\n", " \"action\": \"doFunc\",\n", " \"parameters\": [func_str]\n", @@ -147,6 +205,13 @@ " \"parameters\": [None]\n", " }\n", " send_message_to_iframe(message)\n", + "\n", + "def handle_reset_viewer():\n", + " message = {\n", + " \"action\": \"handleResetViewer\",\n", + " \"parameters\": [None]\n", + " }\n", + " send_message_to_iframe(message)\n", "\n" ], "id": "93be5f1320e953c6", @@ -164,17 +229,36 @@ "cell_type": "code", "source": [ "# Example usage with a material configuration:\n", + "iframe = IFrame(src=URL, width=\"800\", height=\"600\")\n", + "display(iframe)\n" + ], + "id": "6f65f2a33a405f16", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "\n", + "\n", "for material_settings in materials_settings:\n", " GIF_NAME = material_settings[\"name\"] or material_settings[\"material_json\"][\"name\"]\n", - " iframe = IFrame(src=URL, width=\"800\", height=\"600\")\n", - " display(iframe)\n", " # Send the material configuration to the iframe at the specified URL\n", - " set_material_in_iframe(material_json)\n", + " time.sleep(2)\n", + " set_material_in_iframe(material_settings[\"material_json\"])\n", + " time.sleep(2)\n", " toggle_bonds()\n", + " time.sleep(2)\n", " set_camera()\n", - " record_gif( GIF_NAME, 60, 0.05)\n" + " time.sleep(2)\n", + " record_gif( GIF_NAME, 60, 0.05)\n", + "\n", + " # We should wait until the gif is generated and saved before moving to the next material\n", + " time.sleep(30)\n", + " handle_reset_viewer()" ], - "id": "6f65f2a33a405f16", + "id": "bd1e9c79e5457f3b", "outputs": [], "execution_count": null }, @@ -182,7 +266,7 @@ "metadata": {}, "cell_type": "code", "source": "", - "id": "bd1e9c79e5457f3b", + "id": "20f597fe3c2c6887", "outputs": [], "execution_count": null } From de7717ce9711f076dc1fe8cf4b5dc26e6508830f Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Wed, 15 Jan 2025 20:01:49 -0800 Subject: [PATCH 08/27] update: limit wait time --- other/media_generation/wave_control.ipynb | 82 +++++++++++------------ 1 file changed, 40 insertions(+), 42 deletions(-) diff --git a/other/media_generation/wave_control.ipynb b/other/media_generation/wave_control.ipynb index 772f4a729..99dc03f31 100644 --- a/other/media_generation/wave_control.ipynb +++ b/other/media_generation/wave_control.ipynb @@ -123,47 +123,25 @@ "from IPython.display import display, IFrame, Javascript, HTML\n", "import json\n", "\n", - "from utils.visualize import visualize_materials\n", + "import time\n", + "import os\n", "\n", - "def reset_iframe():\n", - " \"\"\"\n", - " Resets the iframe content by reloading it.\n", - " \"\"\"\n", - " js_code = \"\"\"\n", - " (function() {\n", - " const iframe = document.querySelector('iframe');\n", - " if (iframe) {\n", - " iframe.src = iframe.src; // Reload the iframe\n", - " console.log('Iframe reset');\n", - " } else {\n", - " console.error('Iframe not found');\n", - " }\n", - " })();\n", - " \"\"\"\n", - " display(Javascript(js_code))\n", "\n", "def send_message_to_iframe(message):\n", " \"\"\"\n", " Creates and displays an iframe pointing to `url`,\n", " then sends `message` to the iframe via postMessage.\n", " \"\"\"\n", - " # Create and display iframe\n", - "\n", - "\n", - " # Prepare JavaScript code to send message to iframe\n", " js_code = f\"\"\"\n", " (function() {{\n", - " // Wait for the iframe to be available\n", " const iframe = document.querySelector('iframe');\n", " if (iframe && iframe.contentWindow) {{\n", - " setTimeout(() => {{\n", - " try {{\n", - " iframe.contentWindow.postMessage({json.dumps(message)}, '*');\n", - " console.log('Message sent to iframe');\n", - " }} catch (error) {{\n", - " console.error('Error sending message:', error);\n", - " }}\n", - " }}, 1000); // slight delay to ensure iframe is ready\n", + " try {{\n", + " iframe.contentWindow.postMessage({json.dumps(message)}, '*');\n", + " console.log('Message sent to iframe');\n", + " }} catch (error) {{\n", + " alert('Error sending message to iframe. See console for more information.', error);\n", + " }}\n", " }} else {{\n", " console.error('Iframe not found or not ready');\n", " }}\n", @@ -199,6 +177,13 @@ " }\n", " send_message_to_iframe(message)\n", "\n", + "def set_camera_to_fit_cell():\n", + " message = {\n", + " \"action\": \"doWaveFunc\",\n", + " \"parameters\": [\"(wave) => { wave.adjustCamerasAndOrbitControlsToCell(); wave.render(); }\"]\n", + " }\n", + " send_message_to_iframe(message)\n", + "\n", "def toggle_bonds():\n", " message = {\n", " \"action\": \"handleToggleBonds\",\n", @@ -212,7 +197,14 @@ " \"parameters\": [None]\n", " }\n", " send_message_to_iframe(message)\n", - "\n" + "\n", + "def test_do_func():\n", + " func_str = f\"(wave) => {{ alert('do_func'); }}\"\n", + " message = {\n", + " \"action\": \"doFunc\",\n", + " \"parameters\": [func_str]\n", + " }\n", + " send_message_to_iframe(message)\n" ], "id": "93be5f1320e953c6", "outputs": [], @@ -232,7 +224,7 @@ "iframe = IFrame(src=URL, width=\"800\", height=\"600\")\n", "display(iframe)\n" ], - "id": "6f65f2a33a405f16", + "id": "d46c536490a3bdb3", "outputs": [], "execution_count": null }, @@ -240,23 +232,29 @@ "metadata": {}, "cell_type": "code", "source": [ - "\n", "\n", "for material_settings in materials_settings:\n", " GIF_NAME = material_settings[\"name\"] or material_settings[\"material_json\"][\"name\"]\n", - " # Send the material configuration to the iframe at the specified URL\n", - " time.sleep(2)\n", + " handle_reset_viewer()\n", + " time.sleep(1)\n", " set_material_in_iframe(material_settings[\"material_json\"])\n", - " time.sleep(2)\n", + " time.sleep(1)\n", " toggle_bonds()\n", - " time.sleep(2)\n", - " set_camera()\n", - " time.sleep(2)\n", + " time.sleep(1)\n", + " set_camera() # doesn't work\n", + " time.sleep(1)\n", + " set_camera_to_fit_cell() # doesn't work\n", + " time.sleep(1)\n", " record_gif( GIF_NAME, 60, 0.05)\n", - "\n", " # We should wait until the gif is generated and saved before moving to the next material\n", - " time.sleep(30)\n", - " handle_reset_viewer()" + " parent_dir = os.path.abspath(os.path.join(os.getcwd(), os.pardir, os.pardir))\n", + " gif_path = os.path.join(parent_dir, GIF_NAME)\n", + " print(f\"Waiting for gif to be generated in {gif_path}\")\n", + " while not os.path.exists(gif_path):\n", + " time.sleep(1)\n", + " else:\n", + " print(f\"Generated gif for {GIF_NAME}\")\n", + " # time.sleep(30)\n" ], "id": "bd1e9c79e5457f3b", "outputs": [], From 3c1621b59888b190df272d4ba0f9ff032f6a67a1 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Wed, 15 Jan 2025 20:16:30 -0800 Subject: [PATCH 09/27] update: process multiple gifs --- .../gif_creation_multiple.ipynb | 539 ++++++++++++++++++ 1 file changed, 539 insertions(+) create mode 100644 other/media_generation/gif_creation_multiple.ipynb diff --git a/other/media_generation/gif_creation_multiple.ipynb b/other/media_generation/gif_creation_multiple.ipynb new file mode 100644 index 000000000..051475263 --- /dev/null +++ b/other/media_generation/gif_creation_multiple.ipynb @@ -0,0 +1,539 @@ +{ + "cells": [ + { + "metadata": {}, + "cell_type": "code", + "source": "", + "id": "55241e740e879a60", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "# Batch GIF Processing Notebook\n", + "\n", + "This notebook provides functionality to:\n", + "1. Process multiple GIFs from an input folder\n", + "2. Resize all GIFs to specified square dimensions\n", + "3. Add text overlay with the GIF filename and standard text\n", + "4. Add logo overlay at selected positions\n", + "5. Compress the resulting GIFs\n" + ], + "id": "28f8a295c4c03fa5" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## 1. Import of Libraries", + "id": "96b41f843ed53919" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "import re\n", + "from PIL import Image, ImageDraw, ImageFont\n", + "import os\n", + "from IPython.display import display, Image as IPImage\n", + "import numpy as np\n" + ], + "id": "91d7d9b05d472f26", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "import os\n", + "import shutil\n", + "import re\n", + "\n", + "def copy_and_clean_gifs(source_folder, target_folder=\"input\"):\n", + " \"\"\"\n", + " Copy GIF files from source folder to target folder,\n", + " removing numbered duplicates (e.g., removes 'xxx1.gif' if 'xxx.gif' exists)\n", + "\n", + " Args:\n", + " source_folder (str): Path to source folder containing GIFs\n", + " target_folder (str): Path to target folder (defaults to 'input')\n", + " \"\"\"\n", + "\n", + "\n", + " # Ensure target folder exists\n", + " os.makedirs(target_folder, exist_ok=True)\n", + "\n", + " # Get all GIF files from source\n", + " gif_files = [f for f in os.listdir(source_folder) if f.lower().endswith('.gif')]\n", + "\n", + " # Dictionary to store base names and their variations\n", + " file_groups = {}\n", + "\n", + " # Group files by their base names\n", + " for file in gif_files:\n", + " # Remove .gif extension\n", + " base = file[:-4]\n", + " # Check if the filename ends with a number\n", + " match = re.match(r'(.*?)\\d+$', base)\n", + "\n", + " if match:\n", + " # If it has a number, use the part before the number as key\n", + " key = match.group(1).rstrip()\n", + " else:\n", + " # If no number, use the whole base as key\n", + " key = base\n", + "\n", + " if key not in file_groups:\n", + " file_groups[key] = []\n", + " file_groups[key].append(file)\n", + "\n", + " # Copy files, skipping numbered versions if base version exists\n", + " copied_count = 0\n", + " skipped_count = 0\n", + "\n", + " for base_name, variations in file_groups.items():\n", + " # Sort variations to ensure base version (without number) comes first if it exists\n", + " variations.sort(key=lambda x: (len(x), x))\n", + "\n", + " # Copy the first variation (usually the base version)\n", + " source_path = os.path.join(source_folder, variations[0])\n", + " target_path = os.path.join(target_folder, variations[0])\n", + " shutil.copy2(source_path, target_path)\n", + " copied_count += 1\n", + "\n", + " # Count skipped variations\n", + " skipped_count += len(variations) - 1\n", + "\n", + " print(f\"Copied {copied_count} files\")\n", + " if skipped_count > 0:\n", + " print(f\"Skipped {skipped_count} numbered variations\")\n", + "\n", + "# Example usage:\n", + "# copy_and_clean_gifs(\"/path/to/source/folder\")\n", + "# Or with custom target: copy_and_clean_gifs(\"/path/to/source\", \"custom_input\")" + ], + "id": "beafdf7911dee370", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## 2. Input/Output Settings", + "id": "87d687ab58c121c0" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Directory settings\n", + "INPUT_DIR = \"input\"\n", + "OUTPUT_DIR = \"output\"\n", + "ASSETS_DIR = \"assets\"\n", + "\n", + "current_dir = os.getcwd()\n", + "print(current_dir)\n", + "parent_dir = os.path.abspath(os.path.join(os.getcwd(), os.pardir, os.pardir))\n", + "print(parent_dir)\n", + "copy_and_clean_gifs(parent_dir, INPUT_DIR)\n", + "\n", + "# Create directories if they don't exist\n", + "for directory in [INPUT_DIR, OUTPUT_DIR, ASSETS_DIR]:\n", + " os.makedirs(directory, exist_ok=True)\n", + "\n", + "# GIF Settings\n", + "GIF_SIZE = 600 # Size for square output\n", + "QUALITY = 20 # GIF quality (1-100)\n", + "\n", + "# Logo Settings\n", + "LOGO_FILE = \"logo.png\" # Place your logo in files/assets/\n", + "LOGO_POSITION = (15, 10)\n", + "\n", + "# Text Overlay Settings\n", + "FONT_SIZE = 16\n", + "WHITE_COLOR = (255, 255, 255)\n", + "BLACK_COLOR = (0, 0, 0)\n", + "FONT_NAME = \"lucida-grande\"\n", + "\n", + "def create_text_overlays(filename):\n", + " \"\"\"Create text overlays using the GIF filename as text_1\"\"\"\n", + " # Clean up filename by removing extension and replacing underscores/hyphens with spaces\n", + " clean_name = os.path.splitext(filename)[0].replace('_', ' ').replace('-', ' ')\n", + "\n", + " return [\n", + " {\n", + " \"text\": clean_name,\n", + " \"position\": (10, GIF_SIZE - 10 - FONT_SIZE), # Bottom left\n", + " \"font\": FONT_NAME,\n", + " \"color\": WHITE_COLOR,\n", + " \"stroke_width\": 2,\n", + " \"stroke_fill\": BLACK_COLOR\n", + " },\n", + " {\n", + " \"text\": \"Available in our materials bank\",\n", + " \"position\": (GIF_SIZE//2 + 50, GIF_SIZE - 10 - FONT_SIZE), # Bottom right\n", + " \"font\": FONT_NAME,\n", + " \"color\": WHITE_COLOR,\n", + " \"stroke_width\": 2,\n", + " \"stroke_fill\": BLACK_COLOR\n", + " }\n", + " ]\n" + ], + "id": "a1c34d09ad2cceba", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## 3. Font Manager Class", + "id": "4b28ffce16cc2b8f" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "import matplotlib.font_manager as fm\n", + "\n", + "class FontManager:\n", + " \"\"\"Manages fonts for the GIF processor\"\"\"\n", + "\n", + " def __init__(self):\n", + " \"\"\"Initialize font manager and discover available fonts\"\"\"\n", + " self.fonts = self._discover_fonts()\n", + "\n", + " def _discover_fonts(self):\n", + " \"\"\"Discover all available fonts in the system\"\"\"\n", + " fonts = {}\n", + " for font in fm.fontManager.ttflist:\n", + " try:\n", + " name = font.name.lower().replace(' ', '-')\n", + " fonts[name] = font.fname\n", + " except Exception:\n", + " continue\n", + " return fonts\n", + "\n", + " def get_font(self, font_name='default', size=30):\n", + " \"\"\"Get a font by name and size\"\"\"\n", + " try:\n", + " if font_name == 'default':\n", + " return ImageFont.load_default()\n", + "\n", + " if font_name in self.fonts:\n", + " return ImageFont.truetype(self.fonts[font_name], size)\n", + "\n", + " fuzzy_matches = [path for name, path in self.fonts.items()\n", + " if font_name in name and name != 'default']\n", + " if fuzzy_matches:\n", + " return ImageFont.truetype(fuzzy_matches[0], size)\n", + "\n", + " raise ValueError(f\"Font '{font_name}' not found\")\n", + "\n", + " except Exception as e:\n", + " print(f\"Error loading font {font_name}: {str(e)}\")\n", + " return ImageFont.load_default()\n", + "\n", + " def list_fonts(self):\n", + " \"\"\"List all available fonts\"\"\"\n", + " return ['default'] + sorted(list(self.fonts.keys()))\n", + "\n", + " def search_fonts(self, query):\n", + " \"\"\"Search for fonts containing the query string\"\"\"\n", + " query = query.lower()\n", + " matches = [name for name in self.fonts.keys() if query in name]\n", + " return sorted(matches)\n" + ], + "id": "eb7d268efcb8e4be", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## 3.2. List fonts", + "id": "319e3f442312162a" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Initialize font manager and list available fonts\n", + "font_manager = FontManager()\n", + "print(\"Available fonts:\")\n", + "print(font_manager.list_fonts())\n" + ], + "id": "466a49d667f00d13", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## 4. GIF Processor Class", + "id": "fde58fe5fb75c662" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from io import BytesIO\n", + "\n", + "class GIFProcessor:\n", + " def __init__(self, gif_path):\n", + " \"\"\"Initialize with path to GIF file\"\"\"\n", + " self.gif = Image.open(gif_path)\n", + " self.frames = []\n", + " self.durations = []\n", + "\n", + " # Extract all frames and their durations\n", + " try:\n", + " while True:\n", + " self.frames.append(self.gif.copy())\n", + " self.durations.append(self.gif.info.get('duration', 100))\n", + " self.gif.seek(self.gif.tell() + 1)\n", + " except EOFError:\n", + " pass\n", + "\n", + " def resize(self, width, height):\n", + " \"\"\"Resize all frames to specified dimensions\"\"\"\n", + " self.frames = [frame.resize((width, height), Image.Resampling.LANCZOS)\n", + " for frame in self.frames]\n", + " return self\n", + "\n", + " def make_square(self, size=None):\n", + " \"\"\"\n", + " Crop the GIF to a square from the center.\n", + " If size is provided, the output will be resized to size x size.\n", + " If size is None, the square will be sized to the smaller dimension.\n", + " \"\"\"\n", + " if not self.frames:\n", + " return self\n", + "\n", + " # Get dimensions from first frame\n", + " width, height = self.frames[0].size\n", + "\n", + " # Calculate crop box for square\n", + " if width > height:\n", + " # Landscape orientation\n", + " left = (width - height) // 2\n", + " top = 0\n", + " right = left + height\n", + " bottom = height\n", + " else:\n", + " # Portrait orientation\n", + " left = 0\n", + " top = (height - width) // 2\n", + " right = width\n", + " bottom = top + width\n", + "\n", + " # Apply crop to all frames\n", + " self.frames = [frame.crop((left, top, right, bottom)) for frame in self.frames]\n", + "\n", + " # Resize if size is specified\n", + " if size is not None:\n", + " self.frames = [frame.resize((size, size), Image.Resampling.LANCZOS)\n", + " for frame in self.frames]\n", + "\n", + " return self\n", + "\n", + " def add_text(self, text, position, font_path=None, font_size=30,\n", + " color=(255, 255, 255), stroke_width=2, stroke_fill=(0, 0, 0)):\n", + " \"\"\"Add text overlay to all frames\"\"\"\n", + " font_manager = FontManager()\n", + " font = font_manager.get_font(font_name=font_path,size=font_size)\n", + "\n", + " for i, frame in enumerate(self.frames):\n", + " # Convert to RGBA before drawing\n", + " frame_rgba = frame.convert('RGBA')\n", + " draw = ImageDraw.Draw(frame_rgba)\n", + " draw.text(position, text, font=font, fill=color,\n", + " stroke_width=stroke_width, stroke_fill=stroke_fill)\n", + " self.frames[i] = frame_rgba\n", + " return self\n", + "\n", + " def add_image_overlay(self, overlay_path, position):\n", + " \"\"\"Add image overlay to all frames\"\"\"\n", + " overlay = Image.open(overlay_path).convert('RGBA')\n", + "\n", + " for i, frame in enumerate(self.frames):\n", + " frame_rgba = frame.convert('RGBA')\n", + " frame_rgba.paste(overlay, position, overlay)\n", + " self.frames[i] = frame_rgba\n", + " return self\n", + "\n", + " def optimize(self, quality=100):\n", + " if not self.frames:\n", + " return\n", + "\n", + " # Convert frames to RGB mode for saving\n", + " rgb_frames = []\n", + " for frame in self.frames:\n", + " rgb_frame = frame.convert('RGB')\n", + " # Calculate number of colors based on quality\n", + " n_colors = max(min(256, int(256 * (quality / 100))), 2)\n", + "\n", + " # Convert to P mode (palette) with optimized palette\n", + " rgb_frame = rgb_frame.quantize(\n", + " colors=n_colors,\n", + " method=Image.Quantize.MEDIANCUT,\n", + " dither=Image.Dither.FLOYDSTEINBERG\n", + " )\n", + " rgb_frames.append(rgb_frame)\n", + "\n", + " self.frames = rgb_frames\n", + "\n", + " def save(self, output_path, optimize=False, quality=100):\n", + " \"\"\"\n", + " Save the processed GIF with optimization options\n", + "\n", + " Args:\n", + " output_path (str): Path to save the GIF\n", + " optimize (bool): Whether to optimize the GIF\n", + " quality (int): Quality from 1 (worst) to 100 (best).\n", + " Lower quality means smaller file size.\n", + " \"\"\"\n", + " if not self.frames:\n", + " return\n", + " if optimize:\n", + " self.optimize(quality)\n", + " rgb_frames = [frame.convert('RGB') for frame in self.frames]\n", + "\n", + " # Save with optimization\n", + " rgb_frames[0].save(\n", + " output_path,\n", + " save_all=True,\n", + " append_images=rgb_frames[1:],\n", + " optimize=optimize,\n", + " duration=self.durations,\n", + " loop=0,\n", + " format='GIF',\n", + " # Additional optimization parameters\n", + " disposal=2, # Clear the frame before rendering the next\n", + " quality=quality\n", + " )\n", + " print(\"Size on disk:\", f\"{os.path.getsize(output_path) / 1024 / 1024:.2f} MB\")\n", + "\n", + " def display(self):\n", + " \"\"\"Display the current state of the GIF in the notebook\"\"\"\n", + " temp_path = '_temp_display.gif'\n", + " self.save(temp_path)\n", + " display(IPImage(filename=temp_path))\n", + " os.remove(temp_path)\n", + "\n", + " def get_size(self, optimize=False, quality=100):\n", + " \"\"\"Get the size of the processed GIF in bytes without saving to disk\"\"\"\n", + " if self.frames:\n", + " # Convert frames back to RGB mode for saving\n", + " rgb_frames = [frame.convert('RGB') for frame in self.frames]\n", + " with BytesIO() as buffer:\n", + " rgb_frames[0].save(\n", + " buffer,\n", + " save_all=True,\n", + " append_images=rgb_frames[1:],\n", + " optimize=optimize,\n", + " quality=quality,\n", + " duration=self.durations,\n", + " loop=0,\n", + " format='GIF'\n", + " )\n", + " return buffer.tell()\n" + ], + "id": "12609dcf66804351", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## 5. Process All GIFs", + "id": "348711dac2da3b96" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "def process_all_gifs():\n", + " \"\"\"Process all GIFs in the input directory\"\"\"\n", + " # Get logo path\n", + " logo_path = os.path.join(ASSETS_DIR, LOGO_FILE)\n", + " if not os.path.exists(logo_path):\n", + " print(f\"Warning: Logo file not found at {logo_path}\")\n", + " return\n", + "\n", + " # Get all GIF files from input directory\n", + " gif_files = [f for f in os.listdir(INPUT_DIR) if f.lower().endswith('.gif')]\n", + "\n", + " if not gif_files:\n", + " print(\"No GIF files found in input directory\")\n", + " return\n", + "\n", + " print(f\"Found {len(gif_files)} GIF files to process\")\n", + "\n", + " # Process each GIF\n", + " for gif_file in gif_files:\n", + " try:\n", + " print(f\"\\nProcessing: {gif_file}\")\n", + "\n", + " input_path = os.path.join(INPUT_DIR, gif_file)\n", + " output_path = os.path.join(OUTPUT_DIR, f\"processed_{gif_file}\")\n", + "\n", + " # Create GIF processor\n", + " gif_processor = GIFProcessor(input_path)\n", + "\n", + " # Make square and resize\n", + " gif_processor.make_square(size=GIF_SIZE)\n", + "\n", + " # Add text overlays\n", + " text_overlays = create_text_overlays(gif_file)\n", + " for overlay in text_overlays:\n", + " gif_processor.add_text(\n", + " text=overlay[\"text\"],\n", + " position=overlay[\"position\"],\n", + " font_path=overlay[\"font\"],\n", + " font_size=FONT_SIZE,\n", + " color=overlay[\"color\"],\n", + " stroke_width=overlay[\"stroke_width\"],\n", + " stroke_fill=overlay[\"stroke_fill\"]\n", + " )\n", + "\n", + " # Add logo\n", + " gif_processor.add_image_overlay(logo_path, position=LOGO_POSITION)\n", + "\n", + " # Optimize and save\n", + " gif_processor.optimize(quality=QUALITY)\n", + " gif_processor.save(output_path, optimize=False, quality=QUALITY)\n", + "\n", + " print(f\"Successfully processed: {gif_file}\")\n", + "\n", + " except Exception as e:\n", + " print(f\"Error processing {gif_file}: {str(e)}\")\n", + " continue\n", + "\n", + "# Run the batch processing\n", + "process_all_gifs()" + ], + "id": "228b136f3a8379d4", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": "", + "id": "543b54f4f66b74b1", + "outputs": [], + "execution_count": null + } + ], + "metadata": { + "kernelspec": { + "name": "python3", + "language": "python", + "display_name": "Python 3 (ipykernel)" + } + }, + "nbformat": 5, + "nbformat_minor": 9 +} From 06584fc8963606fb173e5994f80096431e861cb5 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Thu, 16 Jan 2025 09:12:09 -0800 Subject: [PATCH 10/27] update: with new set camera to cell --- other/media_generation/wave_control.ipynb | 111 +++++++++++++--------- 1 file changed, 65 insertions(+), 46 deletions(-) diff --git a/other/media_generation/wave_control.ipynb b/other/media_generation/wave_control.ipynb index 99dc03f31..7327dad9d 100644 --- a/other/media_generation/wave_control.ipynb +++ b/other/media_generation/wave_control.ipynb @@ -1,32 +1,39 @@ { "cells": [ { - "metadata": {}, "cell_type": "markdown", - "source": "## 1. Settings of Notebook", - "id": "5542df901e7c76cc" + "id": "5542df901e7c76cc", + "metadata": {}, + "source": [ + "## 1. Settings of Notebook" + ] }, { - "metadata": {}, "cell_type": "code", + "execution_count": 13, + "id": "79ee60afd06b7b3f", + "metadata": {}, + "outputs": [], "source": [ "import time\n", "\n", "URL = \"http://localhost:3002\"\n" - ], - "id": "79ee60afd06b7b3f", - "outputs": [], - "execution_count": null + ] }, { - "metadata": {}, "cell_type": "markdown", - "source": "## 2. Loading of materials", - "id": "a10c370c09229a6b" + "id": "a10c370c09229a6b", + "metadata": {}, + "source": [ + "## 2. Loading of materials" + ] }, { - "metadata": {}, "cell_type": "code", + "execution_count": 14, + "id": "191df844bfc61a4e", + "metadata": {}, + "outputs": [], "source": [ "material_json_1 = {\n", " \"name\": \"New Material\",\n", @@ -105,20 +112,22 @@ "{\"material_json\": material_json_2,\n", " \"name\": \"New Material 2 (SiO).gif\"\n", "}]" - ], - "id": "191df844bfc61a4e", - "outputs": [], - "execution_count": null + ] }, { - "metadata": {}, "cell_type": "markdown", - "source": "## 3. Definitions", - "id": "384f7d7270ecbf53" + "id": "384f7d7270ecbf53", + "metadata": {}, + "source": [ + "## 3. Definitions" + ] }, { - "metadata": {}, "cell_type": "code", + "execution_count": 21, + "id": "93be5f1320e953c6", + "metadata": {}, + "outputs": [], "source": [ "from IPython.display import display, IFrame, Javascript, HTML\n", "import json\n", @@ -179,8 +188,8 @@ "\n", "def set_camera_to_fit_cell():\n", " message = {\n", - " \"action\": \"doWaveFunc\",\n", - " \"parameters\": [\"(wave) => { wave.adjustCamerasAndOrbitControlsToCell(); wave.render(); }\"]\n", + " \"action\": \"handleSetCameraToFitCell\",\n", + " \"parameters\": [None]\n", " }\n", " send_message_to_iframe(message)\n", "\n", @@ -199,38 +208,51 @@ " send_message_to_iframe(message)\n", "\n", "def test_do_func():\n", - " func_str = f\"(wave) => {{ alert('do_func'); }}\"\n", + " func_str = \"() => { alert('do func') }\"\n", " message = {\n", - " \"action\": \"doFunc\",\n", + " \"action\": \"doWaveFunc\",\n", " \"parameters\": [func_str]\n", " }\n", " send_message_to_iframe(message)\n" - ], - "id": "93be5f1320e953c6", - "outputs": [], - "execution_count": null + ] }, { - "metadata": {}, "cell_type": "markdown", - "source": "## 4. Gif Generation", - "id": "592649ebe1e44c46" + "id": "592649ebe1e44c46", + "metadata": {}, + "source": [ + "## 4. Gif Generation" + ] }, { - "metadata": {}, "cell_type": "code", + "execution_count": null, + "id": "d46c536490a3bdb3", + "metadata": {}, + "outputs": [], "source": [ "# Example usage with a material configuration:\n", - "iframe = IFrame(src=URL, width=\"800\", height=\"600\")\n", + "iframe = IFrame(src=URL, width=\"400\", height=\"400\")\n", "display(iframe)\n" - ], - "id": "d46c536490a3bdb3", - "outputs": [], - "execution_count": null + ] }, { + "cell_type": "code", + "execution_count": null, + "id": "be0cc22a", "metadata": {}, + "outputs": [], + "source": [ + "test_do_func()\n", + "set_camera_to_fit_cell()" + ] + }, + { "cell_type": "code", + "execution_count": null, + "id": "bd1e9c79e5457f3b", + "metadata": {}, + "outputs": [], "source": [ "\n", "for material_settings in materials_settings:\n", @@ -255,23 +277,20 @@ " else:\n", " print(f\"Generated gif for {GIF_NAME}\")\n", " # time.sleep(30)\n" - ], - "id": "bd1e9c79e5457f3b", - "outputs": [], - "execution_count": null + ] }, { - "metadata": {}, "cell_type": "code", - "source": "", + "execution_count": null, "id": "20f597fe3c2c6887", + "metadata": {}, "outputs": [], - "execution_count": null + "source": [] } ], "metadata": { "kernelspec": { - "display_name": ".venv-3.11.2", + "display_name": ".venv-3.11", "language": "python", "name": "python3" }, @@ -285,7 +304,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.2" + "version": "3.11.7" } }, "nbformat": 5, From fb60dc40353d0d24d5cc4aa1d397c96ea6919d2b Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Thu, 16 Jan 2025 13:42:50 -0800 Subject: [PATCH 11/27] update: cleanups --- other/media_generation/README.md | 0 .../gif_creation_multiple.ipynb | 67 +++-- ...trol.ipynb => record_gifs_with_wave.ipynb} | 253 ++++++++++-------- 3 files changed, 181 insertions(+), 139 deletions(-) create mode 100644 other/media_generation/README.md rename other/media_generation/{wave_control.ipynb => record_gifs_with_wave.ipynb} (50%) diff --git a/other/media_generation/README.md b/other/media_generation/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/other/media_generation/gif_creation_multiple.ipynb b/other/media_generation/gif_creation_multiple.ipynb index 051475263..31672dae5 100644 --- a/other/media_generation/gif_creation_multiple.ipynb +++ b/other/media_generation/gif_creation_multiple.ipynb @@ -1,13 +1,5 @@ { "cells": [ - { - "metadata": {}, - "cell_type": "code", - "source": "", - "id": "55241e740e879a60", - "outputs": [], - "execution_count": null - }, { "metadata": {}, "cell_type": "markdown", @@ -133,19 +125,10 @@ "OUTPUT_DIR = \"output\"\n", "ASSETS_DIR = \"assets\"\n", "\n", - "current_dir = os.getcwd()\n", - "print(current_dir)\n", - "parent_dir = os.path.abspath(os.path.join(os.getcwd(), os.pardir, os.pardir))\n", - "print(parent_dir)\n", - "copy_and_clean_gifs(parent_dir, INPUT_DIR)\n", - "\n", - "# Create directories if they don't exist\n", - "for directory in [INPUT_DIR, OUTPUT_DIR, ASSETS_DIR]:\n", - " os.makedirs(directory, exist_ok=True)\n", "\n", "# GIF Settings\n", "GIF_SIZE = 600 # Size for square output\n", - "QUALITY = 20 # GIF quality (1-100)\n", + "QUALITY = 30 # GIF quality (1-100)\n", "\n", "# Logo Settings\n", "LOGO_FILE = \"logo.png\" # Place your logo in files/assets/\n", @@ -153,9 +136,37 @@ "\n", "# Text Overlay Settings\n", "FONT_SIZE = 16\n", - "WHITE_COLOR = (255, 255, 255)\n", - "BLACK_COLOR = (0, 0, 0)\n", - "FONT_NAME = \"lucida-grande\"\n", + "TEXT_COLOR = (255, 255, 255)\n", + "STROKE_COLOR = (0, 0, 0)\n", + "FONT_NAME = \"lucida-grande\" # Use `font_manager.list_fonts()` below to see available fonts\n", + "\n", + "TEXT_2 = \"Available in our materials bank\"" + ], + "id": "8c4fedfe514706bd", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## 2.1. Copy and Clean GIFs", + "id": "839f510115d46896" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# GIFs generated by `record_gifs_with_wave.ipynb` are downloaded to the root of `api-examples`\n", + "# They need to be copied to input directory and pruned from duplications due to possible bugs.\n", + "current_dir = os.getcwd()\n", + "print(current_dir)\n", + "parent_dir = os.path.abspath(os.path.join(os.getcwd(), os.pardir, os.pardir))\n", + "print(parent_dir)\n", + "copy_and_clean_gifs(parent_dir, INPUT_DIR)\n", + "\n", + "# Create directories if they don't exist\n", + "for directory in [INPUT_DIR, OUTPUT_DIR, ASSETS_DIR]:\n", + " os.makedirs(directory, exist_ok=True)\n", "\n", "def create_text_overlays(filename):\n", " \"\"\"Create text overlays using the GIF filename as text_1\"\"\"\n", @@ -167,21 +178,21 @@ " \"text\": clean_name,\n", " \"position\": (10, GIF_SIZE - 10 - FONT_SIZE), # Bottom left\n", " \"font\": FONT_NAME,\n", - " \"color\": WHITE_COLOR,\n", + " \"color\": TEXT_COLOR,\n", " \"stroke_width\": 2,\n", - " \"stroke_fill\": BLACK_COLOR\n", + " \"stroke_fill\": STROKE_COLOR\n", " },\n", " {\n", - " \"text\": \"Available in our materials bank\",\n", + " \"text\": TEXT_2,\n", " \"position\": (GIF_SIZE//2 + 50, GIF_SIZE - 10 - FONT_SIZE), # Bottom right\n", " \"font\": FONT_NAME,\n", - " \"color\": WHITE_COLOR,\n", + " \"color\": TEXT_COLOR,\n", " \"stroke_width\": 2,\n", - " \"stroke_fill\": BLACK_COLOR\n", + " \"stroke_fill\": STROKE_COLOR\n", " }\n", " ]\n" ], - "id": "a1c34d09ad2cceba", + "id": "247d54c3f6ac24aa", "outputs": [], "execution_count": null }, @@ -262,7 +273,7 @@ "# Initialize font manager and list available fonts\n", "font_manager = FontManager()\n", "print(\"Available fonts:\")\n", - "print(font_manager.list_fonts())\n" + "# print(font_manager.list_fonts())\n" ], "id": "466a49d667f00d13", "outputs": [], diff --git a/other/media_generation/wave_control.ipynb b/other/media_generation/record_gifs_with_wave.ipynb similarity index 50% rename from other/media_generation/wave_control.ipynb rename to other/media_generation/record_gifs_with_wave.ipynb index 7327dad9d..a461e90a9 100644 --- a/other/media_generation/wave_control.ipynb +++ b/other/media_generation/record_gifs_with_wave.ipynb @@ -10,15 +10,16 @@ }, { "cell_type": "code", - "execution_count": 13, "id": "79ee60afd06b7b3f", "metadata": {}, - "outputs": [], "source": [ "import time\n", "\n", - "URL = \"http://localhost:3002\"\n" - ] + "URL = \"http://localhost:3002\"\n", + "JSON_FOLDER = \"input\"" + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -30,89 +31,124 @@ }, { "cell_type": "code", - "execution_count": 14, "id": "191df844bfc61a4e", "metadata": {}, - "outputs": [], "source": [ - "material_json_1 = {\n", - " \"name\": \"New Material\",\n", - " \"basis\": {\n", - " \"elements\": [\n", - " {\"id\": 0, \"value\": \"Si\"},\n", - " {\"id\": 1, \"value\": \"Si\"}\n", - " ],\n", - " \"coordinates\": [\n", - " {\"id\": 0, \"value\": [0, 0, 0]},\n", - " {\"id\": 1, \"value\": [0.25, 0.25, 0.25]}\n", - " ],\n", - " \"units\": \"crystal\",\n", - " \"cell\": [\n", - " [3.34892, 0, 1.9335],\n", - " [1.116307, 3.157392, 1.9335],\n", - " [0, 0, 3.867]\n", - " ],\n", - " \"constraints\": []\n", - " },\n", - " \"lattice\": {\n", - " \"a\": 3.867, \"b\": 3.867, \"c\": 3.867,\n", - " \"alpha\": 60, \"beta\": 90, \"gamma\": 90,\n", - " \"units\": {\"length\": \"angstrom\", \"angle\": \"degree\"},\n", - " \"type\": \"FCC\",\n", - " \"vectors\": {\n", - " \"a\": [3.34892, 0, 1.9335],\n", - " \"b\": [1.116307, 3.157392, 1.9335],\n", - " \"c\": [0, 0, 3.867],\n", - " \"alat\": 1,\n", - " \"units\": \"angstrom\"\n", - " }\n", - " },\n", - " \"isNonPeriodic\": False\n", - "}\n", + "import os\n", + "import json\n", "\n", - "material_json_2 = {\n", - " \"name\": \"Another Material\",\n", - " \"basis\": {\n", - " \"elements\": [\n", - " {\"id\": 0, \"value\": \"Si\"},\n", - " {\"id\": 1, \"value\": \"O\"}\n", - " ],\n", - " \"coordinates\": [\n", - " {\"id\": 0, \"value\": [0, 0, 0]},\n", - " {\"id\": 1, \"value\": [0.15, 0.15, 0.15]}\n", - " ],\n", - " \"units\": \"crystal\",\n", - " \"cell\": [\n", - " [5.0, 0, 0],\n", - " [0, 5.0, 0],\n", - " [0, 0, 5.0]\n", - " ],\n", - " \"constraints\": []\n", - " },\n", - " \"lattice\": {\n", - " \"a\": 5.0, \"b\": 5.0, \"c\": 5.0,\n", - " \"alpha\": 90, \"beta\": 90, \"gamma\": 60,\n", - " \"units\": {\"length\": \"angstrom\", \"angle\": \"degree\"},\n", - " \"type\": \"CUB\",\n", - " \"vectors\": {\n", - " \"a\": [5.0, 0, 0],\n", - " \"b\": [0, 5.0, 0],\n", - " \"c\": [0, 0, 5.0],\n", - " \"alat\": 1,\n", - " \"units\": \"angstrom\"\n", - " }\n", - " },\n", - " \"isNonPeriodic\": False\n", - "}\n", + "def load_json_files_from_folder(folder_path):\n", + " json_files = [f for f in os.listdir(folder_path) if f.endswith('.json')]\n", + " json_data = []\n", + " file_names = []\n", "\n", - "materials_settings = [{\n", - " \"material_json\": material_json_1,\n", - " \"name\": \"New Material 1 (Si2).gif\"\n", - "},\n", - "{\"material_json\": material_json_2,\n", - " \"name\": \"New Material 2 (SiO).gif\"\n", - "}]" - ] + " for json_file in json_files:\n", + " file_path = os.path.join(folder_path, json_file)\n", + " with open(file_path, 'r') as f:\n", + " data = json.load(f)\n", + " json_data.append(data)\n", + " file_names.append(os.path.splitext(json_file)[0])\n", + "\n", + " return json_data, file_names\n", + "\n", + "json_data, file_names = load_json_files_from_folder(JSON_FOLDER)\n", + "\n", + "materials_settings = []\n", + "for data in json_data:\n", + " materials_settings.append({\n", + " \"material_json\": data,\n", + " \"name\": file_names[json_data.index(data)]\n", + " })\n", + "\n", + "#\n", + "# material_json_1 = {\n", + "# \"name\": \"New Material\",\n", + "# \"basis\": {\n", + "# \"elements\": [\n", + "# {\"id\": 0, \"value\": \"Si\"},\n", + "# {\"id\": 1, \"value\": \"Si\"}\n", + "# ],\n", + "# \"coordinates\": [\n", + "# {\"id\": 0, \"value\": [0, 0, 0]},\n", + "# {\"id\": 1, \"value\": [0.25, 0.25, 0.25]}\n", + "# ],\n", + "# \"units\": \"crystal\",\n", + "# \"cell\": [\n", + "# [3.34892, 0, 1.9335],\n", + "# [1.116307, 3.157392, 1.9335],\n", + "# [0, 0, 3.867]\n", + "# ],\n", + "# \"constraints\": []\n", + "# },\n", + "# \"lattice\": {\n", + "# \"a\": 3.867, \"b\": 3.867, \"c\": 3.867,\n", + "# \"alpha\": 60, \"beta\": 90, \"gamma\": 90,\n", + "# \"units\": {\"length\": \"angstrom\", \"angle\": \"degree\"},\n", + "# \"type\": \"FCC\",\n", + "# \"vectors\": {\n", + "# \"a\": [3.34892, 0, 1.9335],\n", + "# \"b\": [1.116307, 3.157392, 1.9335],\n", + "# \"c\": [0, 0, 3.867],\n", + "# \"alat\": 1,\n", + "# \"units\": \"angstrom\"\n", + "# }\n", + "# },\n", + "# \"isNonPeriodic\": False\n", + "# }\n", + "#\n", + "# material_json_2 = {\n", + "# \"name\": \"Another Material\",\n", + "# \"basis\": {\n", + "# \"elements\": [\n", + "# {\"id\": 0, \"value\": \"Si\"},\n", + "# {\"id\": 1, \"value\": \"O\"},\n", + "# {\"id\": 2, \"value\": \"Si\"},\n", + "# {\"id\": 3, \"value\": \"O\"},\n", + "# {\"id\": 4, \"value\": \"Si\"},\n", + "# {\"id\": 5, \"value\": \"O\"}\n", + "# ],\n", + "# \"coordinates\": [\n", + "# {\"id\": 0, \"value\": [0, 0, 0]},\n", + "# {\"id\": 1, \"value\": [0.15, 0.15, 0.15]},\n", + "# {\"id\": 2, \"value\": [0.15, 0.25, 0.25]},\n", + "# {\"id\": 3, \"value\": [0.15, 0.25, 0.10]},\n", + "# {\"id\": 4, \"value\": [0.5, 0.5, 0.5]},\n", + "# {\"id\": 5, \"value\": [0.55, 0.55, 0.60]}\n", + "# ],\n", + "# \"units\": \"crystal\",\n", + "# \"cell\": [\n", + "# [7.0, 0, 0],\n", + "# [0, 7.0, 0],\n", + "# [0, 0, 7.0]\n", + "# ],\n", + "# \"constraints\": []\n", + "# },\n", + "# \"lattice\": {\n", + "# \"a\": 7.0, \"b\": 7.0, \"c\": 7.0,\n", + "# \"alpha\": 90, \"beta\": 90, \"gamma\": 90,\n", + "# \"units\": {\"length\": \"angstrom\", \"angle\": \"degree\"},\n", + "# \"type\": \"CUB\",\n", + "# \"vectors\": {\n", + "# \"a\": [7.0, 0, 0],\n", + "# \"b\": [0, 7.0, 0],\n", + "# \"c\": [0, 0, 7.0],\n", + "# \"alat\": 1,\n", + "# \"units\": \"angstrom\"\n", + "# }\n", + "# },\n", + "# \"isNonPeriodic\": False\n", + "# }\n", + "#\n", + "# materials_settings = [{\n", + "# \"material_json\": material_json_1,\n", + "# \"name\": \"New Material 1 (Si2).gif\"\n", + "# },\n", + "# {\"material_json\": material_json_2,\n", + "# \"name\": \"New Material 2 (SiO).gif\"\n", + "# }]" + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -124,10 +160,8 @@ }, { "cell_type": "code", - "execution_count": 21, "id": "93be5f1320e953c6", "metadata": {}, - "outputs": [], "source": [ "from IPython.display import display, IFrame, Javascript, HTML\n", "import json\n", @@ -178,13 +212,19 @@ " }\n", " send_message_to_iframe(message)\n", "\n", - "def set_camera( pos=(0, 5, 0), target=(0, 0, 0)):\n", - " func_str = f\"(wave) => {{ alert('set_camera'); wave.camera.position.set({pos[0]}, {pos[1]}, {pos[2]}); wave.rebuildScene(); }}\"\n", + "def set_camera( pos=(15, 15, 15), target=(0, 0, 0)):\n", + " func_str = f\"camera.position.set({pos[0]},{pos[1]},{pos[2]})\"\n", " message = {\n", - " \"action\": \"doFunc\",\n", + " \"action\": \"doWaveFunc\",\n", " \"parameters\": [func_str]\n", " }\n", " send_message_to_iframe(message)\n", + " # func_str = f\"camera.lookAt({target[0]},{target[1]},{target[2]})\"\n", + " # message = {\n", + " # \"action\": \"doFunc\",\n", + " # \"parameters\": [func_str]\n", + " # }\n", + " # send_message_to_iframe(message)\n", "\n", "def set_camera_to_fit_cell():\n", " message = {\n", @@ -208,13 +248,15 @@ " send_message_to_iframe(message)\n", "\n", "def test_do_func():\n", - " func_str = \"() => { alert('do func') }\"\n", + " func_str = \"camera.position.set(0,16,0)\"\n", " message = {\n", " \"action\": \"doWaveFunc\",\n", " \"parameters\": [func_str]\n", " }\n", " send_message_to_iframe(message)\n" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -226,46 +268,33 @@ }, { "cell_type": "code", - "execution_count": null, "id": "d46c536490a3bdb3", "metadata": {}, - "outputs": [], "source": [ "# Example usage with a material configuration:\n", - "iframe = IFrame(src=URL, width=\"400\", height=\"400\")\n", + "iframe = IFrame(src=URL, width=\"600\", height=\"600\")\n", "display(iframe)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "be0cc22a", - "metadata": {}, + ], "outputs": [], - "source": [ - "test_do_func()\n", - "set_camera_to_fit_cell()" - ] + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "id": "bd1e9c79e5457f3b", "metadata": {}, - "outputs": [], "source": [ "\n", "for material_settings in materials_settings:\n", - " GIF_NAME = material_settings[\"name\"] or material_settings[\"material_json\"][\"name\"]\n", + " GIF_NAME = material_settings[\"name\"]+\".gif\" or material_settings[\"material_json\"][\"name\"] + \".gif\"\n", " handle_reset_viewer()\n", " time.sleep(1)\n", " set_material_in_iframe(material_settings[\"material_json\"])\n", " time.sleep(1)\n", " toggle_bonds()\n", " time.sleep(1)\n", - " set_camera() # doesn't work\n", + " # set_camera() # doesn't work\n", " time.sleep(1)\n", - " set_camera_to_fit_cell() # doesn't work\n", + " set_camera_to_fit_cell()\n", " time.sleep(1)\n", " record_gif( GIF_NAME, 60, 0.05)\n", " # We should wait until the gif is generated and saved before moving to the next material\n", @@ -277,15 +306,17 @@ " else:\n", " print(f\"Generated gif for {GIF_NAME}\")\n", " # time.sleep(30)\n" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "id": "20f597fe3c2c6887", "metadata": {}, + "source": "", "outputs": [], - "source": [] + "execution_count": null } ], "metadata": { From 0b17bb0fa089e22d3c5485bed271332ca67acf83 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Thu, 16 Jan 2025 13:45:30 -0800 Subject: [PATCH 12/27] update: add short readme --- other/media_generation/README.md | 13 +++++++++++++ ...multiple.ipynb => gif_processing_multiple.ipynb} | 0 2 files changed, 13 insertions(+) rename other/media_generation/{gif_creation_multiple.ipynb => gif_processing_multiple.ipynb} (100%) diff --git a/other/media_generation/README.md b/other/media_generation/README.md index e69de29bb..efa85c921 100644 --- a/other/media_generation/README.md +++ b/other/media_generation/README.md @@ -0,0 +1,13 @@ +# GIFs generation with Materials visualization + +## Setup materials + +Place materials JSON files in the `input` directory. + +Name them with a short name that will appear at bottom left of GIF. + +## Generate GIFs + +Run `record_gifs_with_wave.ipynb` to generate GIFs. + +Run `gif_processing_multiple.ipynb` to add overlays and generate final GIFs. diff --git a/other/media_generation/gif_creation_multiple.ipynb b/other/media_generation/gif_processing_multiple.ipynb similarity index 100% rename from other/media_generation/gif_creation_multiple.ipynb rename to other/media_generation/gif_processing_multiple.ipynb From 870cc8f8a21c469a5dd6e2c276b4e77bd2aaaf3a Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Thu, 16 Jan 2025 13:54:57 -0800 Subject: [PATCH 13/27] update: urls --- other/media_generation/README.md | 6 ++++++ other/media_generation/record_gifs_with_wave.ipynb | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/other/media_generation/README.md b/other/media_generation/README.md index efa85c921..bdf1ce5e7 100644 --- a/other/media_generation/README.md +++ b/other/media_generation/README.md @@ -6,6 +6,12 @@ Place materials JSON files in the `input` directory. Name them with a short name that will appear at bottom left of GIF. +## Start wave.js + +Run wave.js (from commit: https://github.com/Exabyte-io/wave.js/pull/154/commits/83fc6fd587345728cb35bae0a0ebb258ef1491b6) locally (default port 3002 -- used in notebooks). + +Or keep URL of the Netlify deployment. + ## Generate GIFs Run `record_gifs_with_wave.ipynb` to generate GIFs. diff --git a/other/media_generation/record_gifs_with_wave.ipynb b/other/media_generation/record_gifs_with_wave.ipynb index a461e90a9..e63077c17 100644 --- a/other/media_generation/record_gifs_with_wave.ipynb +++ b/other/media_generation/record_gifs_with_wave.ipynb @@ -15,7 +15,7 @@ "source": [ "import time\n", "\n", - "URL = \"http://localhost:3002\"\n", + "URL = \"https://deploy-preview-154--wave-js.netlify.app/\" # or \"http://localhost:3002/\"\n", "JSON_FOLDER = \"input\"" ], "outputs": [], From a920e17cd8d8981964951b66283a78cc1fdb6817 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 17 Jan 2025 17:01:51 -0800 Subject: [PATCH 14/27] update: add nb to generate interfaces --- other/media_generation/create_materials.ipynb | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 other/media_generation/create_materials.ipynb diff --git a/other/media_generation/create_materials.ipynb b/other/media_generation/create_materials.ipynb new file mode 100644 index 000000000..2dac40a3d --- /dev/null +++ b/other/media_generation/create_materials.ipynb @@ -0,0 +1,125 @@ +{ + "cells": [ + { + "metadata": {}, + "cell_type": "code", + "source": [ + "import os\n", + "from pymatgen.ext.matproj import MPRester\n", + "from mat3ra.made.tools.build.slab import SlabConfiguration, get_terminations, create_slab\n", + "from mat3ra.made.tools.build.interface import InterfaceConfiguration, ZSLStrainMatchingParameters, \\\n", + " ZSLStrainMatchingInterfaceBuilder, ZSLStrainMatchingInterfaceBuilderParameters\n", + "from utils.visualize import visualize_materials\n", + "from utils.jupyterlite import set_materials\n", + "\n", + "from mat3ra.standata.materials import Materials\n", + "from mat3ra.made.material import Material\n", + "\n", + "materials = Materials.get_by_categories(\"3D\")\n", + "\n", + "# Map lattice type to Miller Index\n", + "\n", + "lattice_to_miller = {\n", + " \"CUB\": (1, 1, 1),\n", + " \"FCC\": (1, 1, 1),\n", + " \"HEX\": (0, 0, 1),\n", + " \"TRI\": (1, 1, 1)\n", + "}\n", + "\n", + "# Create interfaces\n", + "for i, substrate_json in enumerate(materials):\n", + " for j, film_json in enumerate(materials):\n", + " if i != j:\n", + " print(f\"Creating interface between {substrate_json['name']} and {film_json['name']}\")\n", + "\n", + " substrate = Material(substrate_json)\n", + " film = Material(film_json)\n", + " # Define slab and interface parameters\n", + " slab_params = {\n", + " \"miller_indices\": lattice_to_miller[substrate.lattice.type],\n", + " \"thickness\": 3,\n", + " \"vacuum\": 15.0,\n", + " \"xy_supercell_matrix\": [[1, 0], [0, 1]],\n", + " \"use_conventional_cell\": True,\n", + " \"use_orthogonal_z\": True\n", + " }\n", + "\n", + " interface_params = {\n", + " \"distance_z\": 3.0,\n", + " \"vacuum\": 0.0,\n", + " \"max_area\": 150,\n", + " \"max_area_tol\": 0.10,\n", + " \"max_angle_tol\": 0.04,\n", + " \"max_length_tol\": 0.04\n", + " }\n", + "\n", + "\n", + " # Create slab configurations\n", + " substrate_slab_config = SlabConfiguration(bulk=substrate, **slab_params)\n", + " film_slab_config = SlabConfiguration(bulk=film, **slab_params)\n", + " try:\n", + " # Get terminations\n", + " substrate_terminations = get_terminations(substrate_slab_config)\n", + " film_terminations = get_terminations(film_slab_config)\n", + "\n", + " # Create slabs\n", + " substrate_slabs = [create_slab(substrate_slab_config, t) for t in substrate_terminations]\n", + " film_slabs = [create_slab(film_slab_config, t) for t in film_terminations]\n", + "\n", + " # Select termination pair (example: first pair)\n", + " termination_pair = (film_terminations[0], substrate_terminations[0])\n", + "\n", + " # Create interface configuration\n", + " interface_config = InterfaceConfiguration(\n", + " film_configuration=film_slab_config,\n", + " substrate_configuration=substrate_slab_config,\n", + " film_termination=termination_pair[0],\n", + " substrate_termination=termination_pair[1],\n", + " distance_z=interface_params[\"distance_z\"],\n", + " vacuum=interface_params[\"vacuum\"]\n", + " )\n", + "\n", + " # Set strain matching parameters\n", + " zsl_params = ZSLStrainMatchingParameters(\n", + " max_area=interface_params[\"max_area\"],\n", + " max_area_tol=interface_params[\"max_area_tol\"],\n", + " max_angle_tol=interface_params[\"max_angle_tol\"],\n", + " max_length_tol=interface_params[\"max_length_tol\"]\n", + " )\n", + "\n", + " # Generate interfaces\n", + " builder = ZSLStrainMatchingInterfaceBuilder(\n", + " build_parameters=ZSLStrainMatchingInterfaceBuilderParameters(strain_matching_parameters=zsl_params)\n", + " )\n", + " interfaces = builder.get_materials(configuration=interface_config)\n", + "\n", + " # Visualize and save the interfaces\n", + " interface = interfaces[0]\n", + " visualize_materials(interface, repetitions=[1, 1, 1])\n", + " # set_materials(interface)\n", + " except Exception as e:\n", + " print(f\"Error creating interface between {substrate.name} and {film.name}: {e}\")" + ], + "id": "2774de5c47160056", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": "", + "id": "3572ccacbb4fd25f", + "outputs": [], + "execution_count": null + } + ], + "metadata": { + "kernelspec": { + "name": "python3", + "language": "python", + "display_name": "Python 3 (ipykernel)" + } + }, + "nbformat": 5, + "nbformat_minor": 9 +} From 854b7184695bb9196e5e8a2342ee0992b559f309 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 17 Jan 2025 18:40:56 -0800 Subject: [PATCH 15/27] update: fix naming --- other/media_generation/create_materials.ipynb | 223 ++++++++++++------ .../gif_processing_multiple.ipynb | 12 +- .../record_gifs_with_wave.ipynb | 6 +- other/media_generation/symbols_map.json | 3 + 4 files changed, 167 insertions(+), 77 deletions(-) create mode 100644 other/media_generation/symbols_map.json diff --git a/other/media_generation/create_materials.ipynb b/other/media_generation/create_materials.ipynb index 2dac40a3d..5cc689645 100644 --- a/other/media_generation/create_materials.ipynb +++ b/other/media_generation/create_materials.ipynb @@ -4,6 +4,8 @@ "metadata": {}, "cell_type": "code", "source": [ + "import json\n", + "import re\n", "import os\n", "from pymatgen.ext.matproj import MPRester\n", "from mat3ra.made.tools.build.slab import SlabConfiguration, get_terminations, create_slab\n", @@ -14,9 +16,17 @@ "\n", "from mat3ra.standata.materials import Materials\n", "from mat3ra.made.material import Material\n", + "from mat3ra.made.tools.modify import wrap_to_unit_cell\n", "\n", "materials = Materials.get_by_categories(\"3D\")\n", "\n", + "# Load the symbols from the JSON file\n", + "with open('symbols_map.json', 'r') as file:\n", + " symbols = json.load(file)\n", + "\n", + "# Extract the \"OVER\" symbol\n", + "SLASH_SYMBOL = symbols[\"/\"]\n", + "\n", "# Map lattice type to Miller Index\n", "\n", "lattice_to_miller = {\n", @@ -26,91 +36,156 @@ " \"TRI\": (1, 1, 1)\n", "}\n", "\n", + "\n", + "def generate_interface(substrate_json, film_json):\n", + " print(f\"Creating interface: {substrate_json['name']} and {film_json['name']}\")\n", + "\n", + " substrate = Material(substrate_json)\n", + " film = Material(film_json)\n", + "\n", + "\n", + " substrate_miller_indices = lattice_to_miller[substrate.lattice.type] if substrate.lattice.type in lattice_to_miller else (0, 0, 1)\n", + " film_miller_indices = lattice_to_miller[film.lattice.type] if film.lattice.type in lattice_to_miller else (0, 0, 1)\n", + "\n", + " # Get material names before the \",\" -- the formula\n", + " substrate_name = re.match(r'[^,]*', substrate.name).group(0) + str(substrate_miller_indices).replace(\", \", \"\")\n", + " film_name = re.match(r'[^,]*', film.name).group(0) + str(film_miller_indices).replace(\", \", \"\")\n", + "\n", + " interface_name = f\"{film_name}{SLASH_SYMBOL}{substrate_name}\"\n", + " print(f\"Interface name: {interface_name}\")\n", + "\n", + "\n", + " # Define slab and interface parameters\n", + " film_params = {\n", + " \"miller_indices\": film_miller_indices,\n", + " \"thickness\": 3,\n", + " \"vacuum\": 15.0,\n", + " \"xy_supercell_matrix\": [[1, 0], [0, 1]],\n", + " \"use_conventional_cell\": True,\n", + " \"use_orthogonal_z\": True\n", + " }\n", + "\n", + " substrate_params = {\n", + " \"miller_indices\": substrate_miller_indices,\n", + " \"thickness\": 3,\n", + " \"vacuum\": 15.0,\n", + " \"xy_supercell_matrix\": [[1, 0], [0, 1]],\n", + " \"use_conventional_cell\": True,\n", + " \"use_orthogonal_z\": True\n", + " }\n", + "\n", + "\n", + " interface_params = {\n", + " \"distance_z\": 3.0,\n", + " \"vacuum\": 1.0,\n", + " \"max_area\": 150,\n", + " \"max_area_tol\": 0.10,\n", + " \"max_angle_tol\": 0.04,\n", + " \"max_length_tol\": 0.04\n", + " }\n", + "\n", + "\n", + " # Create slab configurations\n", + " substrate_slab_config = SlabConfiguration(bulk=substrate, **substrate_params)\n", + " film_slab_config = SlabConfiguration(bulk=film, **film_params)\n", + " try:\n", + " # Get terminations\n", + " substrate_terminations = get_terminations(substrate_slab_config)\n", + " film_terminations = get_terminations(film_slab_config)\n", + "\n", + " # Create slabs\n", + " # substrate_slabs = [create_slab(substrate_slab_config, t) for t in substrate_terminations]\n", + " # film_slabs = [create_slab(film_slab_config, t) for t in film_terminations]\n", + "\n", + " # Select termination pair (example: first pair)\n", + " termination_pair = (film_terminations[0], substrate_terminations[0])\n", + "\n", + " # Create interface configuration\n", + " interface_config = InterfaceConfiguration(\n", + " film_configuration=film_slab_config,\n", + " substrate_configuration=substrate_slab_config,\n", + " film_termination=termination_pair[0],\n", + " substrate_termination=termination_pair[1],\n", + " distance_z=interface_params[\"distance_z\"],\n", + " vacuum=interface_params[\"vacuum\"]\n", + " )\n", + "\n", + " # Set strain matching parameters\n", + " zsl_params = ZSLStrainMatchingParameters(\n", + " max_area=interface_params[\"max_area\"],\n", + " max_area_tol=interface_params[\"max_area_tol\"],\n", + " max_angle_tol=interface_params[\"max_angle_tol\"],\n", + " max_length_tol=interface_params[\"max_length_tol\"]\n", + " )\n", + "\n", + " # Generate interfaces\n", + " builder = ZSLStrainMatchingInterfaceBuilder(\n", + " build_parameters=ZSLStrainMatchingInterfaceBuilderParameters(strain_matching_parameters=zsl_params)\n", + " )\n", + " interfaces = builder.get_materials(configuration=interface_config)\n", + "\n", + " # Visualize and save the interfaces\n", + " interface = interfaces[0]\n", + " interface = wrap_to_unit_cell(interface)\n", + " visualize_materials(interface, repetitions=[1, 1, 1])\n", + "\n", + " interface.name = interface_name\n", + " set_materials(interface)\n", + " except Exception as e:\n", + " print(f\"Error creating interface between {substrate.name} and {film.name}: {e}\")\n" + ], + "id": "f0c47155aa2a5343", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## Generate interfaces between all pairs of materials", + "id": "2d27d1cce04b6bc4" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ "# Create interfaces\n", - "for i, substrate_json in enumerate(materials):\n", - " for j, film_json in enumerate(materials):\n", - " if i != j:\n", - " print(f\"Creating interface between {substrate_json['name']} and {film_json['name']}\")\n", - "\n", - " substrate = Material(substrate_json)\n", - " film = Material(film_json)\n", - " # Define slab and interface parameters\n", - " slab_params = {\n", - " \"miller_indices\": lattice_to_miller[substrate.lattice.type],\n", - " \"thickness\": 3,\n", - " \"vacuum\": 15.0,\n", - " \"xy_supercell_matrix\": [[1, 0], [0, 1]],\n", - " \"use_conventional_cell\": True,\n", - " \"use_orthogonal_z\": True\n", - " }\n", - "\n", - " interface_params = {\n", - " \"distance_z\": 3.0,\n", - " \"vacuum\": 0.0,\n", - " \"max_area\": 150,\n", - " \"max_area_tol\": 0.10,\n", - " \"max_angle_tol\": 0.04,\n", - " \"max_length_tol\": 0.04\n", - " }\n", - "\n", - "\n", - " # Create slab configurations\n", - " substrate_slab_config = SlabConfiguration(bulk=substrate, **slab_params)\n", - " film_slab_config = SlabConfiguration(bulk=film, **slab_params)\n", - " try:\n", - " # Get terminations\n", - " substrate_terminations = get_terminations(substrate_slab_config)\n", - " film_terminations = get_terminations(film_slab_config)\n", - "\n", - " # Create slabs\n", - " substrate_slabs = [create_slab(substrate_slab_config, t) for t in substrate_terminations]\n", - " film_slabs = [create_slab(film_slab_config, t) for t in film_terminations]\n", - "\n", - " # Select termination pair (example: first pair)\n", - " termination_pair = (film_terminations[0], substrate_terminations[0])\n", - "\n", - " # Create interface configuration\n", - " interface_config = InterfaceConfiguration(\n", - " film_configuration=film_slab_config,\n", - " substrate_configuration=substrate_slab_config,\n", - " film_termination=termination_pair[0],\n", - " substrate_termination=termination_pair[1],\n", - " distance_z=interface_params[\"distance_z\"],\n", - " vacuum=interface_params[\"vacuum\"]\n", - " )\n", - "\n", - " # Set strain matching parameters\n", - " zsl_params = ZSLStrainMatchingParameters(\n", - " max_area=interface_params[\"max_area\"],\n", - " max_area_tol=interface_params[\"max_area_tol\"],\n", - " max_angle_tol=interface_params[\"max_angle_tol\"],\n", - " max_length_tol=interface_params[\"max_length_tol\"]\n", - " )\n", - "\n", - " # Generate interfaces\n", - " builder = ZSLStrainMatchingInterfaceBuilder(\n", - " build_parameters=ZSLStrainMatchingInterfaceBuilderParameters(strain_matching_parameters=zsl_params)\n", - " )\n", - " interfaces = builder.get_materials(configuration=interface_config)\n", - "\n", - " # Visualize and save the interfaces\n", - " interface = interfaces[0]\n", - " visualize_materials(interface, repetitions=[1, 1, 1])\n", - " # set_materials(interface)\n", - " except Exception as e:\n", - " print(f\"Error creating interface between {substrate.name} and {film.name}: {e}\")" + "# for i, substrate_json in enumerate(materials):\n", + "# for j, film_json in enumerate(materials):\n", + "# if i != j:\n", + "# generate_interface(substrate_json, film_json)\n" ], - "id": "2774de5c47160056", + "id": "82f16d004d364af3", "outputs": [], "execution_count": null }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## Generate interfaces between random pairs of materials", + "id": "8c63d6310b33335c" + }, { "metadata": {}, "cell_type": "code", - "source": "", + "source": [ + "import random\n", + "num_pairs = 10\n", + "\n", + "for _ in range(num_pairs):\n", + " substrate_json, film_json = random.sample(materials, 2)\n", + " generate_interface(substrate_json, film_json)" + ], "id": "3572ccacbb4fd25f", "outputs": [], "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": "", + "id": "36307e16f113f5e8", + "outputs": [], + "execution_count": null } ], "metadata": { diff --git a/other/media_generation/gif_processing_multiple.ipynb b/other/media_generation/gif_processing_multiple.ipynb index 31672dae5..3bc8478b8 100644 --- a/other/media_generation/gif_processing_multiple.ipynb +++ b/other/media_generation/gif_processing_multiple.ipynb @@ -156,6 +156,8 @@ "metadata": {}, "cell_type": "code", "source": [ + "import json\n", + "\n", "# GIFs generated by `record_gifs_with_wave.ipynb` are downloaded to the root of `api-examples`\n", "# They need to be copied to input directory and pruned from duplications due to possible bugs.\n", "current_dir = os.getcwd()\n", @@ -164,6 +166,14 @@ "print(parent_dir)\n", "copy_and_clean_gifs(parent_dir, INPUT_DIR)\n", "\n", + "# Some symbols needed to be encoded and decoded\n", + "# Load the symbols from the JSON file\n", + "with open('symbols_map.json', 'r') as file:\n", + " symbols = json.load(file)\n", + "\n", + "# Extract the \"OVER\" symbol\n", + "SLASH_SYMBOL = symbols[\"/\"]\n", + "\n", "# Create directories if they don't exist\n", "for directory in [INPUT_DIR, OUTPUT_DIR, ASSETS_DIR]:\n", " os.makedirs(directory, exist_ok=True)\n", @@ -171,7 +181,7 @@ "def create_text_overlays(filename):\n", " \"\"\"Create text overlays using the GIF filename as text_1\"\"\"\n", " # Clean up filename by removing extension and replacing underscores/hyphens with spaces\n", - " clean_name = os.path.splitext(filename)[0].replace('_', ' ').replace('-', ' ')\n", + " clean_name = os.path.splitext(filename)[0].replace(SLASH_SYMBOL, \"/\")\n", "\n", " return [\n", " {\n", diff --git a/other/media_generation/record_gifs_with_wave.ipynb b/other/media_generation/record_gifs_with_wave.ipynb index e63077c17..ef75ae57e 100644 --- a/other/media_generation/record_gifs_with_wave.ipynb +++ b/other/media_generation/record_gifs_with_wave.ipynb @@ -16,7 +16,7 @@ "import time\n", "\n", "URL = \"https://deploy-preview-154--wave-js.netlify.app/\" # or \"http://localhost:3002/\"\n", - "JSON_FOLDER = \"input\"" + "JSON_FOLDER = \"uploads\"" ], "outputs": [], "execution_count": null @@ -57,7 +57,8 @@ "for data in json_data:\n", " materials_settings.append({\n", " \"material_json\": data,\n", - " \"name\": file_names[json_data.index(data)]\n", + " \"name\": file_names[json_data.index(data)] # name of the file\n", + " # \"name\": data[\"name\"] # name of the material\n", " })\n", "\n", "#\n", @@ -301,6 +302,7 @@ " parent_dir = os.path.abspath(os.path.join(os.getcwd(), os.pardir, os.pardir))\n", " gif_path = os.path.join(parent_dir, GIF_NAME)\n", " print(f\"Waiting for gif to be generated in {gif_path}\")\n", + "\n", " while not os.path.exists(gif_path):\n", " time.sleep(1)\n", " else:\n", diff --git a/other/media_generation/symbols_map.json b/other/media_generation/symbols_map.json new file mode 100644 index 000000000..59cd9da32 --- /dev/null +++ b/other/media_generation/symbols_map.json @@ -0,0 +1,3 @@ +{ + "/": "OVER" +} From 4bf5f1c9a4fc3e1b65495cb00338003c3c1afe67 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Fri, 31 Jan 2025 17:34:15 -0800 Subject: [PATCH 16/27] chore: visualize from side --- other/media_generation/create_materials.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/other/media_generation/create_materials.ipynb b/other/media_generation/create_materials.ipynb index 5cc689645..0721069ab 100644 --- a/other/media_generation/create_materials.ipynb +++ b/other/media_generation/create_materials.ipynb @@ -127,7 +127,7 @@ " # Visualize and save the interfaces\n", " interface = interfaces[0]\n", " interface = wrap_to_unit_cell(interface)\n", - " visualize_materials(interface, repetitions=[1, 1, 1])\n", + " visualize_materials([{\"material\": interface, \"rotation\" : \"-90x\"},{\"material\": interface}])\n", "\n", " interface.name = interface_name\n", " set_materials(interface)\n", @@ -169,7 +169,7 @@ "cell_type": "code", "source": [ "import random\n", - "num_pairs = 10\n", + "num_pairs = 20\n", "\n", "for _ in range(num_pairs):\n", " substrate_json, film_json = random.sample(materials, 2)\n", From 2cc9b776760293779453d10f682063a354daf4ee Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Sun, 2 Feb 2025 19:41:02 -0800 Subject: [PATCH 17/27] update: add smarter generation and processing --- other/media_generation/create_materials.ipynb | 10 +-- .../gif_processing_multiple.ipynb | 64 ++++++++++++++++++- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/other/media_generation/create_materials.ipynb b/other/media_generation/create_materials.ipynb index 0721069ab..a16519b2e 100644 --- a/other/media_generation/create_materials.ipynb +++ b/other/media_generation/create_materials.ipynb @@ -42,7 +42,8 @@ "\n", " substrate = Material(substrate_json)\n", " film = Material(film_json)\n", - "\n", + " substrate_dimensionality = \"2D\" if \"2D\" in substrate.name else \"3D\"\n", + " film_dimensionality = \"2D\" if \"2D\" in film.name else \"3D\"\n", "\n", " substrate_miller_indices = lattice_to_miller[substrate.lattice.type] if substrate.lattice.type in lattice_to_miller else (0, 0, 1)\n", " film_miller_indices = lattice_to_miller[film.lattice.type] if film.lattice.type in lattice_to_miller else (0, 0, 1)\n", @@ -54,11 +55,10 @@ " interface_name = f\"{film_name}{SLASH_SYMBOL}{substrate_name}\"\n", " print(f\"Interface name: {interface_name}\")\n", "\n", - "\n", " # Define slab and interface parameters\n", " film_params = {\n", " \"miller_indices\": film_miller_indices,\n", - " \"thickness\": 3,\n", + " \"thickness\": 1 if film_dimensionality == \"2D\" else 3,\n", " \"vacuum\": 15.0,\n", " \"xy_supercell_matrix\": [[1, 0], [0, 1]],\n", " \"use_conventional_cell\": True,\n", @@ -67,7 +67,7 @@ "\n", " substrate_params = {\n", " \"miller_indices\": substrate_miller_indices,\n", - " \"thickness\": 3,\n", + " \"thickness\": 1 if substrate_dimensionality == \"2D\" else 3,\n", " \"vacuum\": 15.0,\n", " \"xy_supercell_matrix\": [[1, 0], [0, 1]],\n", " \"use_conventional_cell\": True,\n", @@ -77,7 +77,7 @@ "\n", " interface_params = {\n", " \"distance_z\": 3.0,\n", - " \"vacuum\": 1.0,\n", + " \"vacuum\": 3.0,\n", " \"max_area\": 150,\n", " \"max_area_tol\": 0.10,\n", " \"max_angle_tol\": 0.04,\n", diff --git a/other/media_generation/gif_processing_multiple.ipynb b/other/media_generation/gif_processing_multiple.ipynb index 3bc8478b8..cb1b4f6c3 100644 --- a/other/media_generation/gif_processing_multiple.ipynb +++ b/other/media_generation/gif_processing_multiple.ipynb @@ -146,6 +146,59 @@ "outputs": [], "execution_count": null }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## 2.2. Automated Description Generation", + "id": "95a6cefb58ecd9ba" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "# Before running this cell, make sure to run `ollama pull && ollama run ` in the terminal\n", + "import ollama\n", + "import os\n", + "\n", + "model_name = 'llama2'\n", + "\n", + "model_config = {\n", + " 'temperature': 0.1, # Controls randomness (0.0 - 1.0)\n", + " 'top_p': 0.9, # Nucleus sampling parameter\n", + " 'top_k': 40, # Top-k sampling parameter\n", + " 'num_predict': 100, # Max tokens to generate\n", + " 'repeat_penalty': 1.1 # Penalize repetition\n", + "}\n", + "\n", + "def get_raw_completion(prompt, model=model_name, model_config=model_config):\n", + " response = ollama.generate(\n", + " model=model,\n", + " prompt=prompt,\n", + " options=model_config\n", + " )\n", + " return response['response']\n", + "\n", + "\n", + "def add_description_for_file(filename):\n", + " system_prompt = \"\"\"You are a materials science robot. Analyze the given filename\n", + " containing information about materials. Extract information about:\n", + " 1. The full colloquial name of materials involved\n", + " 2. Their crystallographic orientations\n", + " 3. The interface relationship and additional parameters if present\n", + " Output only a short concise accurate description of the material. Within template: \"Interface between Full_Material_Name_X(orientation) and Full_Material_Name_Y(orientation) (), strain-matching supercell.\" Only the description, no explanation beyond that.\"\"\"\n", + "\n", + " user_prompt = f\"\"\"Please analyze this materials science filename: {filename}\n", + " Write a short description of material within template.\"\"\"\n", + "\n", + " response = get_raw_completion(system_prompt + user_prompt)\n", + "\n", + " return response\n", + "\n" + ], + "id": "e6f02079d12a29f4", + "outputs": [], + "execution_count": null + }, { "metadata": {}, "cell_type": "markdown", @@ -475,6 +528,7 @@ "metadata": {}, "cell_type": "code", "source": [ + "\n", "def process_all_gifs():\n", " \"\"\"Process all GIFs in the input directory\"\"\"\n", " # Get logo path\n", @@ -498,7 +552,7 @@ " print(f\"\\nProcessing: {gif_file}\")\n", "\n", " input_path = os.path.join(INPUT_DIR, gif_file)\n", - " output_path = os.path.join(OUTPUT_DIR, f\"processed_{gif_file}\")\n", + " output_path = os.path.join(OUTPUT_DIR, f\"{gif_file}\")\n", "\n", " # Create GIF processor\n", " gif_processor = GIFProcessor(input_path)\n", @@ -526,6 +580,14 @@ " gif_processor.optimize(quality=QUALITY)\n", " gif_processor.save(output_path, optimize=False, quality=QUALITY)\n", "\n", + " # Add description\n", + " filename = text_overlays[0][\"text\"]\n", + " description = add_description_for_file(filename)\n", + " print(f\"Filename: {filename}\")\n", + " print(f\"Generated Description: {description}\")\n", + " with open('descriptions.txt', 'a') as file:\n", + " file.write(f\"Filename: {filename}\\n\" + f\"Generated Description: {description}\\n\")\n", + "\n", " print(f\"Successfully processed: {gif_file}\")\n", "\n", " except Exception as e:\n", From be0361cb686e766d7a320b1cbcde153b9e819952 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Sat, 22 Mar 2025 20:11:09 -0700 Subject: [PATCH 18/27] chore: rename --- .../media_generation/{gif_creation.ipynb => gif_processing.ipynb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename other/media_generation/{gif_creation.ipynb => gif_processing.ipynb} (100%) diff --git a/other/media_generation/gif_creation.ipynb b/other/media_generation/gif_processing.ipynb similarity index 100% rename from other/media_generation/gif_creation.ipynb rename to other/media_generation/gif_processing.ipynb From d5047f85823c9661efa892599fc0f7190c91442d Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Sat, 22 Mar 2025 20:12:17 -0700 Subject: [PATCH 19/27] update: add explanation to running nbs --- other/media_generation/README.md | 35 ++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/other/media_generation/README.md b/other/media_generation/README.md index bdf1ce5e7..4cf1a77ba 100644 --- a/other/media_generation/README.md +++ b/other/media_generation/README.md @@ -1,19 +1,36 @@ # GIFs generation with Materials visualization -## Setup materials +## 1. Setup materials -Place materials JSON files in the `input` directory. +1.1. Place materials JSON files in the `input` directory. -Name them with a short name that will appear at bottom left of GIF. +1.2. Name them with a short name that will appear at bottom left of GIF. -## Start wave.js +Alternatively, you can use `create_materials` notebook that has functionality for random generation of some types of materials. Since it's random, verification of the generated materials is necessary. -Run wave.js (from commit: https://github.com/Exabyte-io/wave.js/pull/154/commits/83fc6fd587345728cb35bae0a0ebb258ef1491b6) locally (default port 3002 -- used in notebooks). +## 2. Start wave.js -Or keep URL of the Netlify deployment. +2.1. Run wave.js (from PR: https://github.com/Exabyte-io/wave.js/pull/165) locally (default port 3002 -- used in notebooks). -## Generate GIFs +Or keep URL of the Netlify deployment in the notebook. -Run `record_gifs_with_wave.ipynb` to generate GIFs. +## 3. Record material GIFs -Run `gif_processing_multiple.ipynb` to add overlays and generate final GIFs. +3.1. Run `record_gifs_with_wave.ipynb` to generate GIFs. + +This notebook will generate GIFs for all JSON materials in the `input` directory and save them in the `output` directory. + +3.2. Wait until the GIFs are downloaded. They will appear at the top level of the repo, because we can't control the saving directory. But the next notebook will automatically move them to the `output` directory. + +## 4. Add overlays and generate final GIFs + +4.1. Store any media files (e.g. images, videos) you want to overlay on the GIFs in the `assets` directory. + +4.2. Run `gif_processing_multiple.ipynb` to add overlays and generate final GIFs. + +This notebook will move the GIFs from the top level to the `output` directory, removing any duplications (judging by the file name), and add overlays with the material names. + + +## Single GIF processing + +If you created a GIF with Materials Designer manually, you can use the `gif_processing_single.ipynb` notebook to add overlays and generate the final GIF. From 545d6dcea960e33c77d9e9d2a438c164a8ff7cfa Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Sat, 22 Mar 2025 20:18:38 -0700 Subject: [PATCH 20/27] chore: add more explanation --- .../gif_processing_multiple.ipynb | 130 ++++++++++++------ 1 file changed, 88 insertions(+), 42 deletions(-) diff --git a/other/media_generation/gif_processing_multiple.ipynb b/other/media_generation/gif_processing_multiple.ipynb index cb1b4f6c3..6bb2b7bdb 100644 --- a/other/media_generation/gif_processing_multiple.ipynb +++ b/other/media_generation/gif_processing_multiple.ipynb @@ -11,7 +11,15 @@ "2. Resize all GIFs to specified square dimensions\n", "3. Add text overlay with the GIF filename and standard text\n", "4. Add logo overlay at selected positions\n", - "5. Compress the resulting GIFs\n" + "5. Compress the resulting GIFs\n", + "\n", + "# Usage\n", + "- Place your GIFs in the `input` directory, or keep them at the root of the `api-examples` folder if they were downloaded with `record_gifs_with_wave.ipynb`.\n", + "- Run All Cells to process the GIFs and save the results in the `output` directory.\n", + "- You can select available fonts for text overlays by running the `font_manager.list_fonts()` command in the 3.2. cell and setting `FONT_NAME` to one of the listed fonts.\n", + "- If you wish to use LLM to generate descriptions for the GIFs, install `ollama` and ensure the service is running and the model is pulled.\n", + "- Descriptions will be saved in `descriptions.txt` in the current working directory.\n", + "- You can customize the logo, text overlays, and GIF quality settings in the respective sections of the notebook.\n" ], "id": "28f8a295c4c03fa5" }, @@ -43,6 +51,7 @@ "import shutil\n", "import re\n", "\n", + "\n", "def copy_and_clean_gifs(source_folder, target_folder=\"input\"):\n", " \"\"\"\n", " Copy GIF files from source folder to target folder,\n", @@ -53,7 +62,6 @@ " target_folder (str): Path to target folder (defaults to 'input')\n", " \"\"\"\n", "\n", - "\n", " # Ensure target folder exists\n", " os.makedirs(target_folder, exist_ok=True)\n", "\n", @@ -125,7 +133,6 @@ "OUTPUT_DIR = \"output\"\n", "ASSETS_DIR = \"assets\"\n", "\n", - "\n", "# GIF Settings\n", "GIF_SIZE = 600 # Size for square output\n", "QUALITY = 30 # GIF quality (1-100)\n", @@ -138,7 +145,7 @@ "FONT_SIZE = 16\n", "TEXT_COLOR = (255, 255, 255)\n", "STROKE_COLOR = (0, 0, 0)\n", - "FONT_NAME = \"lucida-grande\" # Use `font_manager.list_fonts()` below to see available fonts\n", + "FONT_NAME = \"lucida-grande\" # Use `font_manager.list_fonts()` below in 3.2. to see available fonts\n", "\n", "TEXT_2 = \"Available in our materials bank\"" ], @@ -149,7 +156,7 @@ { "metadata": {}, "cell_type": "markdown", - "source": "## 2.2. Automated Description Generation", + "source": "## 2.2. LLM Description Generation", "id": "95a6cefb58ecd9ba" }, { @@ -157,42 +164,64 @@ "cell_type": "code", "source": [ "# Before running this cell, make sure to run `ollama pull && ollama run ` in the terminal\n", - "import ollama\n", + "import importlib.util\n", + "import subprocess\n", "import os\n", "\n", - "model_name = 'llama2'\n", + "# Check if ollama is installed\n", + "ollama_installed = importlib.util.find_spec(\"ollama\") is not None\n", + "\n", "\n", - "model_config = {\n", - " 'temperature': 0.1, # Controls randomness (0.0 - 1.0)\n", - " 'top_p': 0.9, # Nucleus sampling parameter\n", - " 'top_k': 40, # Top-k sampling parameter\n", - " 'num_predict': 100, # Max tokens to generate\n", - " 'repeat_penalty': 1.1 # Penalize repetition\n", - "}\n", + "# Check if ollama service is running\n", + "def is_ollama_running():\n", + " try:\n", + " result = subprocess.run([\"pgrep\", \"-f\", \"ollama\"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n", + " return result.returncode == 0\n", + " except Exception:\n", + " return False\n", "\n", - "def get_raw_completion(prompt, model=model_name, model_config=model_config):\n", - " response = ollama.generate(\n", - " model=model,\n", - " prompt=prompt,\n", - " options=model_config\n", - " )\n", - " return response['response']\n", "\n", + "if ollama_installed and is_ollama_running():\n", + " import ollama\n", "\n", - "def add_description_for_file(filename):\n", - " system_prompt = \"\"\"You are a materials science robot. Analyze the given filename\n", - " containing information about materials. Extract information about:\n", - " 1. The full colloquial name of materials involved\n", - " 2. Their crystallographic orientations\n", - " 3. The interface relationship and additional parameters if present\n", - " Output only a short concise accurate description of the material. Within template: \"Interface between Full_Material_Name_X(orientation) and Full_Material_Name_Y(orientation) (), strain-matching supercell.\" Only the description, no explanation beyond that.\"\"\"\n", + " model_name = 'llama2'\n", "\n", - " user_prompt = f\"\"\"Please analyze this materials science filename: {filename}\n", - " Write a short description of material within template.\"\"\"\n", + " model_config = {\n", + " 'temperature': 0.1, # Controls randomness (0.0 - 1.0)\n", + " 'top_p': 0.9, # Nucleus sampling parameter\n", + " 'top_k': 40, # Top-k sampling parameter\n", + " 'num_predict': 100, # Max tokens to generate\n", + " 'repeat_penalty': 1.1 # Penalize repetition\n", + " }\n", + "\n", + "\n", + " def get_raw_completion(prompt, model=model_name, model_config=model_config):\n", + " response = ollama.generate(\n", + " model=model,\n", + " prompt=prompt,\n", + " options=model_config\n", + " )\n", + " return response['response']\n", "\n", - " response = get_raw_completion(system_prompt + user_prompt)\n", "\n", - " return response\n", + " def add_description_for_file(filename):\n", + " system_prompt = \"\"\"You are a materials science robot. Analyze the given filename\n", + " containing information about materials. Extract information about:\n", + " 1. The full colloquial name of materials involved\n", + " 2. Their crystallographic orientations\n", + " 3. The interface relationship and additional parameters if present\n", + " Output only a short concise accurate description of the material. Within template: \"Interface between Full_Material_Name_X(orientation) and Full_Material_Name_Y(orientation) (), strain-matching supercell.\" Only the description, no explanation beyond that.\"\"\"\n", + "\n", + " user_prompt = f\"\"\"Please analyze this materials science filename: {filename}\n", + " Write a short description of material within template.\"\"\"\n", + "\n", + " response = get_raw_completion(system_prompt + user_prompt)\n", + "\n", + " return response\n", + "else:\n", + " print(\"Ollama is not installed or not running. Skipping description generation.\\n\" +\n", + " \"To use this feature, please install Ollama from `https://ollama.com/download`\\n\" +\n", + " \"Make sure to run `ollama pull && ollama run ` in the terminal\")\n", "\n" ], "id": "e6f02079d12a29f4", @@ -229,8 +258,22 @@ "\n", "# Create directories if they don't exist\n", "for directory in [INPUT_DIR, OUTPUT_DIR, ASSETS_DIR]:\n", - " os.makedirs(directory, exist_ok=True)\n", - "\n", + " os.makedirs(directory, exist_ok=True)" + ], + "id": "4ac1cc7071a612a2", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## 2.2. Define Text Overlays", + "id": "6758710ce94551ef" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ "def create_text_overlays(filename):\n", " \"\"\"Create text overlays using the GIF filename as text_1\"\"\"\n", " # Clean up filename by removing extension and replacing underscores/hyphens with spaces\n", @@ -247,7 +290,7 @@ " },\n", " {\n", " \"text\": TEXT_2,\n", - " \"position\": (GIF_SIZE//2 + 50, GIF_SIZE - 10 - FONT_SIZE), # Bottom right\n", + " \"position\": (GIF_SIZE // 2 + 50, GIF_SIZE - 10 - FONT_SIZE), # Bottom right\n", " \"font\": FONT_NAME,\n", " \"color\": TEXT_COLOR,\n", " \"stroke_width\": 2,\n", @@ -255,7 +298,7 @@ " }\n", " ]\n" ], - "id": "247d54c3f6ac24aa", + "id": "4340871c4e59cc92", "outputs": [], "execution_count": null }, @@ -271,6 +314,7 @@ "source": [ "import matplotlib.font_manager as fm\n", "\n", + "\n", "class FontManager:\n", " \"\"\"Manages fonts for the GIF processor\"\"\"\n", "\n", @@ -299,7 +343,7 @@ " return ImageFont.truetype(self.fonts[font_name], size)\n", "\n", " fuzzy_matches = [path for name, path in self.fonts.items()\n", - " if font_name in name and name != 'default']\n", + " if font_name in name and name != 'default']\n", " if fuzzy_matches:\n", " return ImageFont.truetype(fuzzy_matches[0], size)\n", "\n", @@ -326,7 +370,7 @@ { "metadata": {}, "cell_type": "markdown", - "source": "## 3.2. List fonts", + "source": "### 3.2. List fonts", "id": "319e3f442312162a" }, { @@ -354,6 +398,7 @@ "source": [ "from io import BytesIO\n", "\n", + "\n", "class GIFProcessor:\n", " def __init__(self, gif_path):\n", " \"\"\"Initialize with path to GIF file\"\"\"\n", @@ -373,7 +418,7 @@ " def resize(self, width, height):\n", " \"\"\"Resize all frames to specified dimensions\"\"\"\n", " self.frames = [frame.resize((width, height), Image.Resampling.LANCZOS)\n", - " for frame in self.frames]\n", + " for frame in self.frames]\n", " return self\n", "\n", " def make_square(self, size=None):\n", @@ -408,7 +453,7 @@ " # Resize if size is specified\n", " if size is not None:\n", " self.frames = [frame.resize((size, size), Image.Resampling.LANCZOS)\n", - " for frame in self.frames]\n", + " for frame in self.frames]\n", "\n", " return self\n", "\n", @@ -416,14 +461,14 @@ " color=(255, 255, 255), stroke_width=2, stroke_fill=(0, 0, 0)):\n", " \"\"\"Add text overlay to all frames\"\"\"\n", " font_manager = FontManager()\n", - " font = font_manager.get_font(font_name=font_path,size=font_size)\n", + " font = font_manager.get_font(font_name=font_path, size=font_size)\n", "\n", " for i, frame in enumerate(self.frames):\n", " # Convert to RGBA before drawing\n", " frame_rgba = frame.convert('RGBA')\n", " draw = ImageDraw.Draw(frame_rgba)\n", " draw.text(position, text, font=font, fill=color,\n", - " stroke_width=stroke_width, stroke_fill=stroke_fill)\n", + " stroke_width=stroke_width, stroke_fill=stroke_fill)\n", " self.frames[i] = frame_rgba\n", " return self\n", "\n", @@ -594,6 +639,7 @@ " print(f\"Error processing {gif_file}: {str(e)}\")\n", " continue\n", "\n", + "\n", "# Run the batch processing\n", "process_all_gifs()" ], From fbfe1ff336c7b376abbd8cd68b721d5c12fa78fe Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Sat, 22 Mar 2025 20:19:30 -0700 Subject: [PATCH 21/27] update: add point defects --- other/media_generation/create_materials.ipynb | 179 +++++++++++++++++- 1 file changed, 174 insertions(+), 5 deletions(-) diff --git a/other/media_generation/create_materials.ipynb b/other/media_generation/create_materials.ipynb index a16519b2e..f43756c8b 100644 --- a/other/media_generation/create_materials.ipynb +++ b/other/media_generation/create_materials.ipynb @@ -1,5 +1,17 @@ { "cells": [ + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "# Generation of random materials\n", + "\n", + "Run first few cells to set up definitions.\n", + "\n", + "Setup and run selected cells such as \"Create interfaces\" and \"Generate point defects\" to generate desired materials." + ], + "id": "1ff9a40df2ca55a7" + }, { "metadata": {}, "cell_type": "code", @@ -11,6 +23,7 @@ "from mat3ra.made.tools.build.slab import SlabConfiguration, get_terminations, create_slab\n", "from mat3ra.made.tools.build.interface import InterfaceConfiguration, ZSLStrainMatchingParameters, \\\n", " ZSLStrainMatchingInterfaceBuilder, ZSLStrainMatchingInterfaceBuilderParameters\n", + "\n", "from utils.visualize import visualize_materials\n", "from utils.jupyterlite import set_materials\n", "\n", @@ -45,7 +58,8 @@ " substrate_dimensionality = \"2D\" if \"2D\" in substrate.name else \"3D\"\n", " film_dimensionality = \"2D\" if \"2D\" in film.name else \"3D\"\n", "\n", - " substrate_miller_indices = lattice_to_miller[substrate.lattice.type] if substrate.lattice.type in lattice_to_miller else (0, 0, 1)\n", + " substrate_miller_indices = lattice_to_miller[\n", + " substrate.lattice.type] if substrate.lattice.type in lattice_to_miller else (0, 0, 1)\n", " film_miller_indices = lattice_to_miller[film.lattice.type] if film.lattice.type in lattice_to_miller else (0, 0, 1)\n", "\n", " # Get material names before the \",\" -- the formula\n", @@ -74,7 +88,6 @@ " \"use_orthogonal_z\": True\n", " }\n", "\n", - "\n", " interface_params = {\n", " \"distance_z\": 3.0,\n", " \"vacuum\": 3.0,\n", @@ -84,7 +97,6 @@ " \"max_length_tol\": 0.04\n", " }\n", "\n", - "\n", " # Create slab configurations\n", " substrate_slab_config = SlabConfiguration(bulk=substrate, **substrate_params)\n", " film_slab_config = SlabConfiguration(bulk=film, **film_params)\n", @@ -127,7 +139,7 @@ " # Visualize and save the interfaces\n", " interface = interfaces[0]\n", " interface = wrap_to_unit_cell(interface)\n", - " visualize_materials([{\"material\": interface, \"rotation\" : \"-90x\"},{\"material\": interface}])\n", + " visualize_materials([{\"material\": interface, \"rotation\": \"-90x\"}, {\"material\": interface}])\n", "\n", " interface.name = interface_name\n", " set_materials(interface)\n", @@ -138,6 +150,135 @@ "outputs": [], "execution_count": null }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## Generate point defects", + "id": "10a3ac072b69fa38" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "from mat3ra.made.material import Material\n", + "from mat3ra.made.tools.build.supercell import create_supercell\n", + "from mat3ra.made.tools.build.defect import PointDefectConfiguration, create_defects\n", + "from mat3ra.made.tools.build.defect.builders import PointDefectBuilderParameters\n", + "import random\n", + "\n", + "# Define common substitutions and interstitials\n", + "COMMON_DOPANTS = {\n", + " \"Ni\": [\"Cu\", \"Fe\", \"Co\"],\n", + " \"Si\": [\"Ge\", \"C\"],\n", + " \"Ti\": [\"V\", \"Zr\"],\n", + " \"Al\": [\"Ga\", \"Mg\"],\n", + " \"Ga\": [\"Mn\", \"In\"],\n", + " \"In\": [\"Sn\", \"Sb\"],\n", + " \"Sn\": [\"Pb\", \"Bi\"],\n", + " \"Nb\": [\"Ta\", \"Zr\"],\n", + " \"Mo\": [\"W\", \"Cr\"],\n", + " \"W\": [\"Re\", \"Os\"],\n", + " \"Fe\": [\"Co\", \"Ni\"],\n", + " \"Co\": [\"Ni\", \"Cu\"],\n", + " \"Cu\": [\"Ag\", \"Au\"],\n", + " \"Zn\": [\"Cd\", \"Hg\"],\n", + " \"Mg\": [\"Ca\", \"Sr\"],\n", + " \"Ca\": [\"Ba\", \"Sr\"],\n", + " \"Sr\": [\"Ba\", \"Ra\"],\n", + " \"Ba\": [\"Ra\", \"Pb\"],\n", + "}\n", + "\n", + "COMMON_INTERSTITIALS = [\"B\", \"C\", \"N\", \"O\"]\n", + "\n", + "def get_substitution_candidates(base_elements):\n", + " candidates = set()\n", + " for el in base_elements:\n", + " candidates.update(COMMON_DOPANTS.get(el, []))\n", + " return list(candidates) or COMMON_INTERSTITIALS\n", + "\n", + "def get_supercell_matrix(material):\n", + " if material.lattice.type == \"HEX\":\n", + " return [[3, 0, 0], [0, 3, 0], [0, 0, 1]]\n", + " if material.lattice.a < 3.0:\n", + " return [[3, 0, 0], [0, 3, 0], [0, 0, 3]]\n", + " elif material.lattice.a < 6.0:\n", + " return [[2, 0, 0], [0, 2, 0], [0, 0, 2]]\n", + " else:\n", + " return [[1, 0, 0], [0, 1, 0], [0, 0, 1]]\n", + "\n", + "\n", + "def generate_point_defects(material_json, mode=\"random\", num_defects=None):\n", + " material = Material(material_json)\n", + " base_elements = list(set(material.basis.elements.values))\n", + " substitution_elements = get_substitution_candidates(base_elements)\n", + " interstitial_elements = COMMON_INTERSTITIALS\n", + "\n", + " supercell: Material = create_supercell(material, supercell_matrix=get_supercell_matrix(material))\n", + " if num_defects is None:\n", + " num_elements = len(supercell.basis.elements.values)\n", + " num_defects = min(num_elements // 5, 10) # Limit to 20% of the number of elements\n", + " defect_configs = []\n", + "\n", + " if mode == \"grouped_substitution\":\n", + " dopant = random.choice(substitution_elements)\n", + " target_atoms = random.sample(supercell.basis.elements.to_array_of_values_with_ids(), num_defects)\n", + " for atom in target_atoms:\n", + " defect_configs.append({\n", + " \"defect_type\": \"substitution\",\n", + " \"site_id\": atom.id,\n", + " \"chemical_element\": dopant,\n", + " })\n", + "\n", + " elif mode == \"frenkel_cluster\":\n", + " element = random.choice(base_elements)\n", + " atoms_of_element = supercell.basis.elements.get_elements_by_value(element)\n", + "\n", + " if len(atoms_of_element) < num_defects:\n", + " num_defects = len(atoms_of_element)\n", + "\n", + " for atom in random.sample(atoms_of_element, num_defects):\n", + " defect_configs.append({\n", + " \"defect_type\": \"vacancy\",\n", + " \"site_id\": atom.id,\n", + " })\n", + " defect_configs.append({\n", + " \"defect_type\": \"interstitial\",\n", + " \"approximate_coordinate\": [random.uniform(0, 1) for _ in range(3)],\n", + " \"chemical_element\": element,\n", + " })\n", + "\n", + " else: # random mode\n", + " for _ in range(num_defects):\n", + " defect_type = random.choice([\"vacancy\", \"interstitial\", \"substitution\"])\n", + " if defect_type == \"vacancy\":\n", + " site = random.choice(supercell.basis.coordinates.to_array_of_values_with_ids())\n", + " defect_configs.append({\n", + " \"defect_type\": \"vacancy\",\n", + " \"site_id\": site.id,\n", + " })\n", + " elif defect_type == \"interstitial\":\n", + " defect_configs.append({\n", + " \"defect_type\": \"voronoi_interstitial\",\n", + " \"approximate_coordinate\": [random.uniform(0, 1) for _ in range(3)],\n", + " \"chemical_element\": random.choice(interstitial_elements),\n", + " })\n", + " elif defect_type == \"substitution\":\n", + " site = random.choice(supercell.basis.coordinates.to_array_of_values_with_ids())\n", + " defect_configs.append({\n", + " \"defect_type\": \"substitution\",\n", + " \"site_id\": site.id,\n", + " \"chemical_element\": random.choice(substitution_elements),\n", + " })\n", + "\n", + " configurations = [PointDefectConfiguration.from_dict(supercell, d) for d in defect_configs]\n", + " builder_parameters = PointDefectBuilderParameters(center_defect=False)\n", + "\n", + " return create_defects(builder_parameters=builder_parameters, configurations=configurations)\n" + ], + "id": "409ece78e162a33d", + "outputs": [], + "execution_count": null + }, { "metadata": {}, "cell_type": "markdown", @@ -169,6 +310,7 @@ "cell_type": "code", "source": [ "import random\n", + "\n", "num_pairs = 20\n", "\n", "for _ in range(num_pairs):\n", @@ -179,13 +321,40 @@ "outputs": [], "execution_count": null }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Generate point defects in random materials", + "id": "73d4d8d67f9e92b4" + }, { "metadata": {}, "cell_type": "code", - "source": "", + "source": [ + "import random\n", + "num_materials = 5\n", + "num_defects = None\n", + "random_materials = random.sample(materials, num_materials)\n", + "\n", + "for material_json in random_materials:\n", + " try:\n", + " material_with_defect = generate_point_defects(material_json, mode=\"random\", num_defects=num_defects)\n", + " visualize_materials([{\"material\": material_with_defect},{\"material\": material_with_defect, \"rotation\": \"-90x,60y\"}])\n", + " set_materials(material_with_defect)\n", + " except Exception as e:\n", + " print(f\"Error generating defects for material {material_json['name']}: {e}\")" + ], "id": "36307e16f113f5e8", "outputs": [], "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": "", + "id": "173593e52d53c683", + "outputs": [], + "execution_count": null } ], "metadata": { From 414f7e76d1e7fab0d9be30f5db106d01f3f09534 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Sun, 23 Mar 2025 21:18:21 -0700 Subject: [PATCH 22/27] update: adjust according to new messages --- .../record_gifs_with_wave.ipynb | 142 +++--------------- 1 file changed, 22 insertions(+), 120 deletions(-) diff --git a/other/media_generation/record_gifs_with_wave.ipynb b/other/media_generation/record_gifs_with_wave.ipynb index ef75ae57e..77dce0924 100644 --- a/other/media_generation/record_gifs_with_wave.ipynb +++ b/other/media_generation/record_gifs_with_wave.ipynb @@ -5,6 +5,16 @@ "id": "5542df901e7c76cc", "metadata": {}, "source": [ + "# GIF Generation with Wave.js\n", + "\n", + "This notebook allows to record rotating GIFs of materials using the Wave.js viewer.\n", + "\n", + "## Usage\n", + "1. Set the URL of the Wave.js viewer (either local or deployed).\n", + "2. Set the folder containing the JSON files of the materials.\n", + "3. Run the notebook to generate GIFs for each material in the specified folder.\n", + "\n", + "\n", "## 1. Settings of Notebook" ] }, @@ -13,9 +23,8 @@ "id": "79ee60afd06b7b3f", "metadata": {}, "source": [ - "import time\n", - "\n", - "URL = \"https://deploy-preview-154--wave-js.netlify.app/\" # or \"http://localhost:3002/\"\n", + "# URL = \"https://deploy-preview-165--wave-js.netlify.app/\" # or\n", + "URL = \"http://localhost:3002/\"\n", "JSON_FOLDER = \"uploads\"" ], "outputs": [], @@ -34,9 +43,6 @@ "id": "191df844bfc61a4e", "metadata": {}, "source": [ - "import os\n", - "import json\n", - "\n", "def load_json_files_from_folder(folder_path):\n", " json_files = [f for f in os.listdir(folder_path) if f.endswith('.json')]\n", " json_data = []\n", @@ -59,94 +65,7 @@ " \"material_json\": data,\n", " \"name\": file_names[json_data.index(data)] # name of the file\n", " # \"name\": data[\"name\"] # name of the material\n", - " })\n", - "\n", - "#\n", - "# material_json_1 = {\n", - "# \"name\": \"New Material\",\n", - "# \"basis\": {\n", - "# \"elements\": [\n", - "# {\"id\": 0, \"value\": \"Si\"},\n", - "# {\"id\": 1, \"value\": \"Si\"}\n", - "# ],\n", - "# \"coordinates\": [\n", - "# {\"id\": 0, \"value\": [0, 0, 0]},\n", - "# {\"id\": 1, \"value\": [0.25, 0.25, 0.25]}\n", - "# ],\n", - "# \"units\": \"crystal\",\n", - "# \"cell\": [\n", - "# [3.34892, 0, 1.9335],\n", - "# [1.116307, 3.157392, 1.9335],\n", - "# [0, 0, 3.867]\n", - "# ],\n", - "# \"constraints\": []\n", - "# },\n", - "# \"lattice\": {\n", - "# \"a\": 3.867, \"b\": 3.867, \"c\": 3.867,\n", - "# \"alpha\": 60, \"beta\": 90, \"gamma\": 90,\n", - "# \"units\": {\"length\": \"angstrom\", \"angle\": \"degree\"},\n", - "# \"type\": \"FCC\",\n", - "# \"vectors\": {\n", - "# \"a\": [3.34892, 0, 1.9335],\n", - "# \"b\": [1.116307, 3.157392, 1.9335],\n", - "# \"c\": [0, 0, 3.867],\n", - "# \"alat\": 1,\n", - "# \"units\": \"angstrom\"\n", - "# }\n", - "# },\n", - "# \"isNonPeriodic\": False\n", - "# }\n", - "#\n", - "# material_json_2 = {\n", - "# \"name\": \"Another Material\",\n", - "# \"basis\": {\n", - "# \"elements\": [\n", - "# {\"id\": 0, \"value\": \"Si\"},\n", - "# {\"id\": 1, \"value\": \"O\"},\n", - "# {\"id\": 2, \"value\": \"Si\"},\n", - "# {\"id\": 3, \"value\": \"O\"},\n", - "# {\"id\": 4, \"value\": \"Si\"},\n", - "# {\"id\": 5, \"value\": \"O\"}\n", - "# ],\n", - "# \"coordinates\": [\n", - "# {\"id\": 0, \"value\": [0, 0, 0]},\n", - "# {\"id\": 1, \"value\": [0.15, 0.15, 0.15]},\n", - "# {\"id\": 2, \"value\": [0.15, 0.25, 0.25]},\n", - "# {\"id\": 3, \"value\": [0.15, 0.25, 0.10]},\n", - "# {\"id\": 4, \"value\": [0.5, 0.5, 0.5]},\n", - "# {\"id\": 5, \"value\": [0.55, 0.55, 0.60]}\n", - "# ],\n", - "# \"units\": \"crystal\",\n", - "# \"cell\": [\n", - "# [7.0, 0, 0],\n", - "# [0, 7.0, 0],\n", - "# [0, 0, 7.0]\n", - "# ],\n", - "# \"constraints\": []\n", - "# },\n", - "# \"lattice\": {\n", - "# \"a\": 7.0, \"b\": 7.0, \"c\": 7.0,\n", - "# \"alpha\": 90, \"beta\": 90, \"gamma\": 90,\n", - "# \"units\": {\"length\": \"angstrom\", \"angle\": \"degree\"},\n", - "# \"type\": \"CUB\",\n", - "# \"vectors\": {\n", - "# \"a\": [7.0, 0, 0],\n", - "# \"b\": [0, 7.0, 0],\n", - "# \"c\": [0, 0, 7.0],\n", - "# \"alat\": 1,\n", - "# \"units\": \"angstrom\"\n", - "# }\n", - "# },\n", - "# \"isNonPeriodic\": False\n", - "# }\n", - "#\n", - "# materials_settings = [{\n", - "# \"material_json\": material_json_1,\n", - "# \"name\": \"New Material 1 (Si2).gif\"\n", - "# },\n", - "# {\"material_json\": material_json_2,\n", - "# \"name\": \"New Material 2 (SiO).gif\"\n", - "# }]" + " })" ], "outputs": [], "execution_count": null @@ -155,16 +74,14 @@ "cell_type": "markdown", "id": "384f7d7270ecbf53", "metadata": {}, - "source": [ - "## 3. Definitions" - ] + "source": "## 3. Actions Definitions" }, { "cell_type": "code", "id": "93be5f1320e953c6", "metadata": {}, "source": [ - "from IPython.display import display, IFrame, Javascript, HTML\n", + "from IPython.display import display, IFrame, Javascript\n", "import json\n", "\n", "import time\n", @@ -172,10 +89,6 @@ "\n", "\n", "def send_message_to_iframe(message):\n", - " \"\"\"\n", - " Creates and displays an iframe pointing to `url`,\n", - " then sends `message` to the iframe via postMessage.\n", - " \"\"\"\n", " js_code = f\"\"\"\n", " (function() {{\n", " const iframe = document.querySelector('iframe');\n", @@ -199,7 +112,11 @@ " Uses send_message_to_iframe to send a material configuration\n", " under the \"material\" key to the iframe at `url`.\n", " \"\"\"\n", - " send_message_to_iframe({\"material\": material_json})\n", + " message = {\n", + " \"action\": \"handleSetMaterial\",\n", + " \"parameters\": [material_json]\n", + " }\n", + " send_message_to_iframe(message)\n", "\n", "\n", "def record_gif(filename, rotation_speed=60, frame_duration=0.05):\n", @@ -220,12 +137,6 @@ " \"parameters\": [func_str]\n", " }\n", " send_message_to_iframe(message)\n", - " # func_str = f\"camera.lookAt({target[0]},{target[1]},{target[2]})\"\n", - " # message = {\n", - " # \"action\": \"doFunc\",\n", - " # \"parameters\": [func_str]\n", - " # }\n", - " # send_message_to_iframe(message)\n", "\n", "def set_camera_to_fit_cell():\n", " message = {\n", @@ -293,16 +204,15 @@ " time.sleep(1)\n", " toggle_bonds()\n", " time.sleep(1)\n", - " # set_camera() # doesn't work\n", - " time.sleep(1)\n", " set_camera_to_fit_cell()\n", " time.sleep(1)\n", - " record_gif( GIF_NAME, 60, 0.05)\n", + " record_gif( GIF_NAME)\n", " # We should wait until the gif is generated and saved before moving to the next material\n", " parent_dir = os.path.abspath(os.path.join(os.getcwd(), os.pardir, os.pardir))\n", " gif_path = os.path.join(parent_dir, GIF_NAME)\n", " print(f\"Waiting for gif to be generated in {gif_path}\")\n", "\n", + " # Wait until the gif file is created and downloaded\n", " while not os.path.exists(gif_path):\n", " time.sleep(1)\n", " else:\n", @@ -311,14 +221,6 @@ ], "outputs": [], "execution_count": null - }, - { - "cell_type": "code", - "id": "20f597fe3c2c6887", - "metadata": {}, - "source": "", - "outputs": [], - "execution_count": null } ], "metadata": { From 8bfc6e609f14ba40fa6773ad53b93ffaaf2d21ed Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Mon, 24 Mar 2025 11:39:17 -0700 Subject: [PATCH 23/27] chore: edits to readme --- other/media_generation/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/other/media_generation/README.md b/other/media_generation/README.md index 4cf1a77ba..e477af2ec 100644 --- a/other/media_generation/README.md +++ b/other/media_generation/README.md @@ -2,9 +2,11 @@ ## 1. Setup materials -1.1. Place materials JSON files in the `input` directory. +1.1. Generate materials in Mat3ra JSON format with [Materials Designer](https://materials-designer.mat3ra.com/) or use a script. -1.2. Name them with a short name that will appear at bottom left of GIF. +1.2. Place materials JSON files in the `uploads` directory. + +1.3. Name them with a short name that will appear at bottom left of GIF. Alternatively, you can use `create_materials` notebook that has functionality for random generation of some types of materials. Since it's random, verification of the generated materials is necessary. From b92ab8b9c99c9446267e46dc4c4bd52104c8f817 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Mon, 24 Mar 2025 11:49:08 -0700 Subject: [PATCH 24/27] chore: add description --- .../media_generation/record_gifs_with_wave.ipynb | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/other/media_generation/record_gifs_with_wave.ipynb b/other/media_generation/record_gifs_with_wave.ipynb index 77dce0924..9b4e8d65f 100644 --- a/other/media_generation/record_gifs_with_wave.ipynb +++ b/other/media_generation/record_gifs_with_wave.ipynb @@ -43,6 +43,7 @@ "id": "191df844bfc61a4e", "metadata": {}, "source": [ + "import os, json\n", "def load_json_files_from_folder(folder_path):\n", " json_files = [f for f in os.listdir(folder_path) if f.endswith('.json')]\n", " json_data = []\n", @@ -82,11 +83,8 @@ "metadata": {}, "source": [ "from IPython.display import display, IFrame, Javascript\n", - "import json\n", "\n", "import time\n", - "import os\n", - "\n", "\n", "def send_message_to_iframe(message):\n", " js_code = f\"\"\"\n", @@ -175,7 +173,9 @@ "id": "592649ebe1e44c46", "metadata": {}, "source": [ - "## 4. Gif Generation" + "## 4. Gif Generation\n", + "\n", + "### 4.1. Load the iframe with the Wave.js viewer" ] }, { @@ -190,12 +190,17 @@ "outputs": [], "execution_count": null }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "### 4.2. Generate GIFs for each material", + "id": "fb71708af42b49e0" + }, { "cell_type": "code", "id": "bd1e9c79e5457f3b", "metadata": {}, "source": [ - "\n", "for material_settings in materials_settings:\n", " GIF_NAME = material_settings[\"name\"]+\".gif\" or material_settings[\"material_json\"][\"name\"] + \".gif\"\n", " handle_reset_viewer()\n", From 3d3b87c6db51d0efeeaa4258709244d2600a43fd Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Mon, 24 Mar 2025 12:02:20 -0700 Subject: [PATCH 25/27] update: add random MI generation --- other/media_generation/create_materials.ipynb | 44 +++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/other/media_generation/create_materials.ipynb b/other/media_generation/create_materials.ipynb index f43756c8b..b5ba9172b 100644 --- a/other/media_generation/create_materials.ipynb +++ b/other/media_generation/create_materials.ipynb @@ -19,11 +19,11 @@ "import json\n", "import re\n", "import os\n", + "import random\n", "from pymatgen.ext.matproj import MPRester\n", "from mat3ra.made.tools.build.slab import SlabConfiguration, get_terminations, create_slab\n", "from mat3ra.made.tools.build.interface import InterfaceConfiguration, ZSLStrainMatchingParameters, \\\n", " ZSLStrainMatchingInterfaceBuilder, ZSLStrainMatchingInterfaceBuilderParameters\n", - "\n", "from utils.visualize import visualize_materials\n", "from utils.jupyterlite import set_materials\n", "\n", @@ -40,14 +40,29 @@ "# Extract the \"OVER\" symbol\n", "SLASH_SYMBOL = symbols[\"/\"]\n", "\n", - "# Map lattice type to Miller Index\n", "\n", - "lattice_to_miller = {\n", - " \"CUB\": (1, 1, 1),\n", - " \"FCC\": (1, 1, 1),\n", - " \"HEX\": (0, 0, 1),\n", - " \"TRI\": (1, 1, 1)\n", - "}\n", + "def get_random_miller_index(lattice_type):\n", + " \"\"\"\n", + " Get a random Miller index for a given lattice type with a probability distribution\n", + " \"\"\"\n", + " lattice_to_miller = {\n", + " \"CUB\": [{(1, 1, 1): 0.5}, {(1, 1, 0): 0.4}, {(1, 0, 0): 0.1}],\n", + " \"FCC\": [{(1, 1, 1): 0.5}, {(1, 1, 0): 0.3}, {(0, 0, 1): 0.2}],\n", + " \"HEX\": [{(0, 0, 1): 0.9}, {(1, 1, 1): 0.1}],\n", + " \"TRI\": [{(1, 1, 1): 0.5}, {(1, 1, 0): 0.3}, {(0, 0, 1): 0.2}],\n", + " \"_\": (0, 0, 1)\n", + " }\n", + "\n", + " if lattice_type not in lattice_to_miller:\n", + " return lattice_to_miller[\"_\"]\n", + "\n", + " miller_indices = lattice_to_miller[lattice_type]\n", + "\n", + " if isinstance(miller_indices, list):\n", + " choices, weights = zip(*[(k, v) for d in miller_indices for k, v in d.items()])\n", + " return random.choices(choices, weights=weights, k=1)[0]\n", + " else:\n", + " return miller_indices\n", "\n", "\n", "def generate_interface(substrate_json, film_json):\n", @@ -58,9 +73,8 @@ " substrate_dimensionality = \"2D\" if \"2D\" in substrate.name else \"3D\"\n", " film_dimensionality = \"2D\" if \"2D\" in film.name else \"3D\"\n", "\n", - " substrate_miller_indices = lattice_to_miller[\n", - " substrate.lattice.type] if substrate.lattice.type in lattice_to_miller else (0, 0, 1)\n", - " film_miller_indices = lattice_to_miller[film.lattice.type] if film.lattice.type in lattice_to_miller else (0, 0, 1)\n", + " substrate_miller_indices = get_random_miller_index(substrate.lattice.type)\n", + " film_miller_indices = get_random_miller_index(film.lattice.type)\n", "\n", " # Get material names before the \",\" -- the formula\n", " substrate_name = re.match(r'[^,]*', substrate.name).group(0) + str(substrate_miller_indices).replace(\", \", \"\")\n", @@ -125,7 +139,7 @@ " # Set strain matching parameters\n", " zsl_params = ZSLStrainMatchingParameters(\n", " max_area=interface_params[\"max_area\"],\n", - " max_area_tol=interface_params[\"max_area_tol\"],\n", + " max_area_ratio_tol=interface_params[\"max_area_tol\"],\n", " max_angle_tol=interface_params[\"max_angle_tol\"],\n", " max_length_tol=interface_params[\"max_length_tol\"]\n", " )\n", @@ -190,12 +204,14 @@ "\n", "COMMON_INTERSTITIALS = [\"B\", \"C\", \"N\", \"O\"]\n", "\n", + "\n", "def get_substitution_candidates(base_elements):\n", " candidates = set()\n", " for el in base_elements:\n", " candidates.update(COMMON_DOPANTS.get(el, []))\n", " return list(candidates) or COMMON_INTERSTITIALS\n", "\n", + "\n", "def get_supercell_matrix(material):\n", " if material.lattice.type == \"HEX\":\n", " return [[3, 0, 0], [0, 3, 0], [0, 0, 1]]\n", @@ -332,6 +348,7 @@ "cell_type": "code", "source": [ "import random\n", + "\n", "num_materials = 5\n", "num_defects = None\n", "random_materials = random.sample(materials, num_materials)\n", @@ -339,7 +356,8 @@ "for material_json in random_materials:\n", " try:\n", " material_with_defect = generate_point_defects(material_json, mode=\"random\", num_defects=num_defects)\n", - " visualize_materials([{\"material\": material_with_defect},{\"material\": material_with_defect, \"rotation\": \"-90x,60y\"}])\n", + " visualize_materials(\n", + " [{\"material\": material_with_defect}, {\"material\": material_with_defect, \"rotation\": \"-90x,60y\"}])\n", " set_materials(material_with_defect)\n", " except Exception as e:\n", " print(f\"Error generating defects for material {material_json['name']}: {e}\")" From e58e90dae794787f96fe0bc8ca54d31cd4d6c042 Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Thu, 8 May 2025 20:58:41 -0700 Subject: [PATCH 26/27] update: new made --- other/media_generation/create_materials.ipynb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/other/media_generation/create_materials.ipynb b/other/media_generation/create_materials.ipynb index b5ba9172b..01f0ea2b0 100644 --- a/other/media_generation/create_materials.ipynb +++ b/other/media_generation/create_materials.ipynb @@ -68,13 +68,13 @@ "def generate_interface(substrate_json, film_json):\n", " print(f\"Creating interface: {substrate_json['name']} and {film_json['name']}\")\n", "\n", - " substrate = Material(substrate_json)\n", - " film = Material(film_json)\n", + " substrate = Material.create(substrate_json)\n", + " film = Material.create(film_json)\n", " substrate_dimensionality = \"2D\" if \"2D\" in substrate.name else \"3D\"\n", " film_dimensionality = \"2D\" if \"2D\" in film.name else \"3D\"\n", "\n", - " substrate_miller_indices = get_random_miller_index(substrate.lattice.type)\n", - " film_miller_indices = get_random_miller_index(film.lattice.type)\n", + " substrate_miller_indices = get_random_miller_index(substrate.lattice.type.name)\n", + " film_miller_indices = get_random_miller_index(film.lattice.type.name)\n", "\n", " # Get material names before the \",\" -- the formula\n", " substrate_name = re.match(r'[^,]*', substrate.name).group(0) + str(substrate_miller_indices).replace(\", \", \"\")\n", @@ -224,7 +224,7 @@ "\n", "\n", "def generate_point_defects(material_json, mode=\"random\", num_defects=None):\n", - " material = Material(material_json)\n", + " material = Material.create(material_json)\n", " base_elements = list(set(material.basis.elements.values))\n", " substitution_elements = get_substitution_candidates(base_elements)\n", " interstitial_elements = COMMON_INTERSTITIALS\n", From 7c465b3ba26f9a5a096fc59add42ec59c1809d7b Mon Sep 17 00:00:00 2001 From: VsevolodX Date: Thu, 8 May 2025 21:04:48 -0700 Subject: [PATCH 27/27] chore: fix relative import --- other/generate_gifs/__init__.py | 0 other/generate_gifs/gif_processing.ipynb | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 other/generate_gifs/__init__.py diff --git a/other/generate_gifs/__init__.py b/other/generate_gifs/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/other/generate_gifs/gif_processing.ipynb b/other/generate_gifs/gif_processing.ipynb index b049a0ded..ee6bc1a8c 100644 --- a/other/generate_gifs/gif_processing.ipynb +++ b/other/generate_gifs/gif_processing.ipynb @@ -184,7 +184,7 @@ "metadata": {}, "cell_type": "code", "source": [ - "from other.media_generation.utils.font_manager import FontManager\n", + "from other.generate_gifs.utils.font_manager import FontManager\n", "\n", "# Initialize font manager and list available fonts\n", "font_manager = FontManager()\n", @@ -243,7 +243,7 @@ "metadata": {}, "cell_type": "code", "source": [ - "from other.media_generation.utils.gif_processor import GIFProcessor\n", + "from other.generate_gifs.utils.gif_processor import GIFProcessor\n", "\n", "\n", "def process_all_gifs():\n",