Thursday, 5 November 2026

Python Coding Challenge - Question with Answer (ID 051126)

 



Explanation:

Step 1: Understanding the any() Function

The any() function checks whether at least one element in an iterable evaluates to True.

Syntax:
any(iterable)

- Returns True if at least one element is truthy.
- Returns False if all elements are falsy or the iterable is empty.

Step 2: Understanding the List
[0, "", None, [], 5]


This list contains five different values:
Element Boolean Value Explanation
0 False Zero is falsy
"" False Empty string is falsy
None False Represents no value
[] False Empty list is falsy
5 True Non-zero numbers are truthy

Step 3: How any() Evaluates the Elements

Python checks the elements one by one:
1. 0 → False
2. "" → False
3. None → False
4. [ ] → False
5. 5 → True
As soon as Python finds one truthy value, any() returns True.

Step 4: Understanding the print() Function
print(any([0, "", None, [], 5]))

Since the last element 5 is truthy, the any() function returns True.

3. Final Output
Output: True
True

Books: AUTOMATING EXCEL WITH PYTHON

Wednesday, 4 November 2026

Python Coding challenge - Day 1304| What is the output of the following Python Code?

 



Code Explanation:

1. Creating a Class
class Wallet:
- Defines a class named Wallet.
- A class acts as a blueprint for creating objects.
- Here, the class represents a wallet containing some amount of cash.

2. Defining the Constructor
    def __init__(self, cash):

- Defines the special method __init__().
- It is automatically called when a new Wallet object is created.
- The cash parameter receives the initial amount.
- self refers to the current object.

3. Storing the Cash Value
        self.cash = cash
- Assigns the received cash value to the object's cash attribute.
- Each wallet object maintains its own cash amount.
- For example, one wallet can contain 50 while another contains 70.

4. Defining the Addition Method
    def __add__(self, other):

- Defines the special method __add__().
- Python internally calls this method when the + operator is used between two Wallet objects.
- self represents the left-hand object.
- other represents the right-hand object.
For example:
a + b internally behaves like:

a.__add__(b)

5. Returning the Addition Result
        return self.cash + other.cash

- Retrieves the cash value from the first wallet using self.cash.
- Retrieves the cash value from the second wallet using other.cash.
- Adds both values together.
- Returns the calculated result.

6. Creating the First Object
a = Wallet(50)

- Creates an object named a.
- Calls the constructor with cash = 50.
- Stores 50 in a.cash.

7. Creating the Second Object
b = Wallet(70)

- Creates another object named b.
- Passes 70 to the constructor.
- Stores 70 in b.cash.

8. Using the Addition Operator
print(a + b)

- Uses the + operator between two Wallet objects.
- Python automatically calls a.__add__(b).
- The method calculates 50 + 70.
- The returned integer value is 120.
- Finally, print() displays the result.

Final Output
Correct Output
120

Books: 900 Days Python Coding Challenges with Explanation

Python Coding challenge - Day 1303| What is the output of the following Python Code?

 


Code Explanation:

1. Creating an Empty List
funcs = []

- Creates an empty list named funcs.
- This list will store multiple lambda functions.
- Each function will be added to this list using the append() method.

2. Starting the For Loop
for i in range(3):

- The range(3) function generates three values: 0, 1, and 2.
- The loop executes three times.
- During each iteration, the variable i receives the next value.
- These values will be accessed by the lambda functions.

3. Creating Lambda Functions
    funcs.append(lambda: i)

- lambda creates an anonymous function without using the def keyword.
- This lambda function does not accept any arguments.
- It simply returns the current value of i when called.
- append() stores the newly created function inside the funcs list.
Important: The lambda functions do not immediately store the values 0, 1, and 2. They refer to the same variable i, which is looked up when the function is executed.

4. Calling the First Lambda Function
print(funcs[0]())

- funcs[0] accesses the first lambda function in the list.
- The additional () executes that function.
- By this time, the loop has completed.
- The final value of i is 2.
- Therefore, the function returns 2.

5. Calling the Second Lambda Function
print(funcs[1]())

- funcs[1] accesses the second lambda function.
- It also refers to the same variable i.
- Since the loop has already finished, i is still 2.
- Therefore, this function also returns 2.

6. Calling the Third Lambda Function
print(funcs[2]())

- funcs[2] accesses the third lambda function.
- It executes and reads the current value of i.
- The value of i remains 2.
- Therefore, it also returns 2.


Final Output

Correct Output
2
2
2

Flatten Nested Lists in Python 3.15 Using * – A Cleaner Way to Write Python Code

 


Flatten Nested Lists in Python 3.15 Using * – A Cleaner Way to Write Python Code

Python 3.15 introduces an interesting improvement to Python comprehensions: unpacking directly inside list comprehensions using the * operator.

If you frequently work with nested lists, this new syntax provides a cleaner alternative to traditional nested list comprehensions.

Let's understand how it works with a simple example.

1. Understanding Nested Lists

A nested list is a list that contains other lists as its elements.

Consider the following example:

daily_weather = [
    ["☀️", "☁️"],
    ["๐ŸŒง️"],
    ["☀️", "๐ŸŒฉ️"]
]

print(daily_weather)

Output:

[['☀️', '☁️'], ['๐ŸŒง️'], ['☀️', '๐ŸŒฉ️']]

Here, we have three separate lists containing weather conditions.

What if we want to combine all these elements into one single list?

This process is called flattening a list.

2. Traditional Approach (Before Python 3.15)

Before Python 3.15, we commonly used nested list comprehensions.

daily_weather = [
    ["☀️", "☁️"],
    ["๐ŸŒง️"],
    ["☀️", "๐ŸŒฉ️"]
]

result = [
    weather
    for day in daily_weather
    for weather in day
]

print(result)

Output:

['☀️', '☁️', '๐ŸŒง️', '☀️', '๐ŸŒฉ️']

How does it work?

  • The first for loop iterates through each inner list.

  • The second for loop accesses every element inside that list.

  • Each element is added to the resulting list.

This approach works perfectly, but Python 3.15 introduces a more compact alternative.

3. Python 3.15: Flatten Lists Using *

Python 3.15 introduces unpacking in comprehensions through PEP 798.

Now, you can use the * operator directly inside a list comprehension.

daily_weather = [
    ["☀️", "☁️"],
    ["๐ŸŒง️"],
    ["☀️", "๐ŸŒฉ️"]
]

result = [*day for day in daily_weather]

print(result)

Output:

['☀️', '☁️', '๐ŸŒง️', '☀️', '๐ŸŒฉ️']

That's it!

The * operator unpacks each inner iterable, adding its individual elements to the resulting list.

4. Before vs. Python 3.15

Traditional approachPython 3.15
[x for d in data for x in d][*d for d in data]
Nested iterationIterable unpacking
More explicitMore compact

Both approaches produce the same flattened list for this example.

5. More Examples

The new syntax works with other iterable types too.

Example 1: Flatten a list of numbers

numbers = [
    [1, 2, 3],
    [4, 5],
    [6, 7, 8]
]

result = [*group for group in numbers]

print(result)

Output:

[1, 2, 3, 4, 5, 6, 7, 8]

Example 2: Flatten a list containing strings

words = [
    ["Python", "Java"],
    ["C++", "JavaScript"]
]

result = [*group for group in words]

print(result)

Output:

['Python', 'Java', 'C++', 'JavaScript']

Example 3: Flatten a list containing empty lists

data = [
    [10, 20],
    [],
    [30, 40]
]

result = [*group for group in data]

print(result)

Output:

[10, 20, 30, 40]

Empty inner iterables simply contribute no elements.

6. Important Things to Remember

  1. This syntax was introduced in Python 3.15 through PEP 798.

  2. It works with list and set comprehensions, dictionary comprehensions, and generator expressions.

  3. The * operator unpacks an iterable into individual elements.

  4. It flattens one level of nesting at a time.

  5. It does not recursively flatten deeply nested lists.

  6. Python 3.14 and earlier do not support this new comprehension syntax.

Important: This is not a recursive flattening solution. If your list contains multiple levels of nested lists, you will need a recursive function or another suitable approach.

7. Conclusion

Python continues to evolve with features that make code more expressive and easier to read.

The new unpacking syntax in Python 3.15 provides another way to flatten nested lists without writing multiple for clauses.

Traditional approach:

[x for group in data for x in group]

Python 3.15 approach:

[*group for group in data]

Both are useful, but the new syntax gives developers another compact option.

Keep learning, keep coding!

Follow CLCODING for more Python tutorials, coding challenges, and programming updates.



Frequently Asked Questions

Q1. What is list flattening in Python?

List flattening is the process of converting a list containing other lists into a single list containing their individual elements.

Q2. Which Python version supports unpacking in comprehensions?

Python 3.15 introduced this feature through PEP 798.

Q3. Does [*group for group in data] flatten multiple nested levels?

No. It flattens only one level. Deeply nested lists require a recursive or multi-level approach.

Q4. Is the traditional nested list comprehension still supported?

Yes. The traditional syntax remains valid and works in earlier Python versions.

Q5. Is the new syntax faster?

It provides a more concise way to express flattening. Do not assume it is faster in every situation; benchmark both approaches for performance-sensitive applications.

Python Coding Challenge - Question with Answer (ID 041126)

 


Explanation:

Step 1: Creating a Set
x = {1, 2, 3}

Here, we create a set named x containing three elements:

{1, 2, 3}

Step 2: Understanding the Union Operator (|)
x | {3, 4}

The | operator performs a Union operation.

Union combines all unique elements from both sets.
- First set: {1, 2, 3}
- Second set: {3, 4}
Result:
{1, 2, 3, 4}


Step 3: Applying the len() Function
len(x | {3, 4})

The len() function counts the number of unique elements.

Result:
4


Step 4: Understanding the Intersection Operator (&)
x & {2, 3}

The & operator performs an Intersection operation.

It returns only the elements that are common to both sets.
- First set: {1, 2, 3}
- Second set: {2, 3}
Result:
{2, 3}

Step 5: Applying len() to the Intersection
len(x & {2, 3})

The intersection contains two elements.

Result:
2


Step 6: Understanding the Complete Expression

Now Python evaluates:
4 - 2
\[
= 2
\]

3. Final Output
Output: 2
2

Books: 100 Python Automation Projects for Smart Developers



Tuesday, 3 November 2026

๐Ÿ Python Pattern Challenge — Day 40

 




๐Ÿ Python Pattern Challenge — Day 40 | Number Wave Pattern

Welcome to Day 40 of the Python Pattern Challenge! ๐Ÿš€

Today, we're creating an interesting Number Wave Pattern using Python. This pattern starts with a complete number sequence, gradually reduces the numbers in each row, and then expands again to create a symmetrical wave-like design.

It's a simple but effective challenge to improve your understanding of loops, number sequences, spacing, and pattern-building logic.

๐ŸŽฏ Today's Challenge

Write a Python program to print exactly the following pattern:


๐Ÿ’ป Python Solution

n = 5 for i in range(1, n + 1): print(" " * (i - 1), end="") print(*range(i, n + 1)) for i in range(n - 1, 0, -1): print(" " * (i - 1), end="") print(*range(i, n + 1))





๐Ÿง  How It Works

1. Creating the upper section

for i in range(1, n + 1):

This loop generates the first five rows, starting from 1 and increasing the starting number in each row.

2. Managing the spacing

print(" " * (i - 1), end="")

This adds leading spaces to shift each row toward the center.

3. Printing the number sequence

print(*range(i, n + 1))

The range() function generates numbers from the current value of i up to 5.

4. Creating the lower section

for i in range(n - 1, 0, -1):

This loop reverses the process, gradually restoring the complete number sequence.

๐Ÿš€ Challenge Yourself

  • Increase the pattern size to 7.

  • Replace numbers with alphabets.

  • Create an inverted version of this pattern.

  • Try implementing the same pattern using a while loop.

  • Take the maximum number as input from the user.

The best way to improve your Python logic is to practice small challenges and experiment with different patterns.

41 Days. 41 Patterns. Stronger Python Logic. ๐Ÿ๐Ÿ”ฅ

Learn • Practice • Grow with CLCODING


Monday, 2 November 2026

Python Coding Challenge - Question with Answer (ID 031126)

 


Explanation:

Step 1: Understanding Complex Numbers

Python supports complex numbers, which consist of two parts:

- Real part – the number without j.

- Imaginary part – the number multiplied by j.

In our code:

z = 3 + 4j

Here:

- 3 is the real part.

- 4 is the imaginary part.

- j represents the imaginary unit.


Step 2: Understanding z.real

z.real

The .real attribute returns the real part of a complex number.

Output:

3.0

Step 3: Understanding z.imag

z.imag

The .imag attribute returns the imaginary coefficient.

Output:

4.0

Step 4: Understanding the Exponent Operator **

Now consider:

z.imag ** 2

Since z.imag = 4.0, Python calculates:

\[

4.0^2 = 16.0

\]

Step 5: Understanding the Complete Expression

z.real + z.imag ** 2

Python follows operator precedence, so exponentiation happens before addition.

\[

3.0 + (4.0)^2

\]

\[

= 3.0 + 16.0

\]

\[

= 19.0

\]

Step 6: Understanding print()

The print() function displays the final calculated value.


3. Final Output

Output: 19.0

19.0

Books: PYTHON LOOPS MASTERY


Python Coding challenge - Day 1302| What is the output of the following Python Code?

 




Code Explanation:

1. Defining a Generator Function
def process():

- Defines a function named process().
- This function contains yield statements, so Python treats it as a Generator Function.
- Unlike a normal function, a generator produces values one at a time instead of returning all values together.

2. First Yield Statement
    yield 10

- The yield keyword produces the value 10.
- The function pauses its execution at this point.
- Its current execution state is saved.
- When the generator is called again, execution resumes from where it stopped.

3. Second Yield Statement
    yield 20

- Produces the next value, which is 20.
- The generator pauses again.
- It remembers its current position and waits for the next request.

4. Return Statement
    return 30
- Ends the generator function.
- The value 30 is not yielded as a normal generator output.
- Instead, it becomes the value carried by the StopIteration exception.
- Once this statement executes, the generator is exhausted.
Important: yield produces values, whereas return terminates the generator.

5. Creating the Generator Object
g = process()

- Calls the generator function.
- Creates a generator object and stores it in g.
- The function body does not execute immediately.
- Execution starts only when we request a value using next().

6. Getting the First Value
print(next(g))
- next() starts executing the generator.
- Execution reaches yield 10.
- The generator pauses and returns 10.
- print() displays the value.
Output:
10

7. Getting the Second Value
print(next(g))
- Calls next() again on the same generator.
- Execution resumes after the first yield.
- It reaches yield 20.
- The generator pauses again and returns 20.
Output:
20


8. Handling Generator Exhaustion
print(next(g, "Finished"))

- Calls next() for the third time.
- The generator resumes after yield 20.
- It executes return 30 and terminates.
- Python raises StopIteration, carrying 30 as its value.
- However, next(g, "Finished") uses "Finished" as the default value when the generator is exhausted.
- Therefore, it prints "Finished" instead of raising an unhandled exception.
Output:
Finished


Final Output
Correct Answer
10
20
Finished

Python Coding challenge - Day 1301| What is the output of the following Python Code?

 

Code Explanation:

1. Defining the Outer Function

def multiplier(n):

- Defines a function named multiplier.
- It accepts one parameter n.
- This function will create another function that remembers the value of n.
- This is the foundation of a Closure in Python.

2. Defining the Inner Function
    def calculate(x):
- Defines an inner function named calculate.
- It accepts one parameter x.
- Since this function is defined inside multiplier(), it can access variables from the outer function.

3. Returning the Calculation
        return x * n
- Multiplies x by n.
- Here, x is a local parameter of calculate().
- n comes from the enclosing multiplier() function.
- Even after the outer function finishes executing, the inner function remembers the value of n.
Important: This ability to remember variables from an enclosing scope is called a closure.

4. Returning the Inner Function
    return calculate
- Returns the inner function itself, not its result.
- Notice that we use calculate, not calculate().
- This allows the returned function to be stored in another variable and called later.

5. Creating the Double Function
double = multiplier(2)
- Calls multiplier() with n = 2.
- The inner function calculate is returned.
- It remembers that n is 2.
- The returned function is stored in double.
So logically:

double(x) = x * 2

6. Creating the Triple Function
triple = multiplier(3)
- Calls multiplier() again, this time with n = 3.
- A new inner function is returned.
- This function remembers n = 3.
- It is stored in the variable triple.
So logically:
triple(x) = x * 3

7. Calling Both Functions
print(double(triple(4)))

Let's evaluate this expression step by step.

Step 1: Execute the innermost function:
triple(4)

Since triple remembers n = 3:

4 * 3 = 12

Step 2: Pass the result to double():
double(12)

Since double remembers n = 2:

12 * 2 = 24

Step 3: Print the final result:
print(24)


Final Output
24

Pydantic Settings: Manage Python Application Configuration Like a Pro

 



Pydantic Settings: Manage Python Application Configuration Like a Pro

SEO Title: Pydantic Settings in Python: Manage Environment Variables and Application Configuration

Meta Description: Learn how to use pydantic-settings in Python to manage environment variables, validate application configuration, load .env files, and build cleaner applications with practical examples.

Keywords: Pydantic Settings, Python environment variables, pydantic-settings tutorial, Python configuration management, Python backend development, Python libraries


Introduction

When building Python applications, configuration management is an important part of writing clean and maintainable code.

You might have different configuration values for:

  • Application names

  • Database connections

  • API keys

  • Debug mode

  • Server ports

  • External service URLs

A common beginner approach is to hardcode these values directly into Python files.

APP_NAME = "CLCODING"
DEBUG = True
PORT = 8000

This works for small projects, but it becomes difficult to maintain as your application grows.

What if you need different configurations for development, testing, and production?

This is where Pydantic Settings becomes useful.

What is Pydantic Settings?

pydantic-settings is a Python library that helps you manage application configuration using environment variables, .env files, and other settings sources.

It builds on Pydantic's validation capabilities, allowing you to define configuration using Python type hints.

Instead of manually reading and converting environment variables, you can define a settings class and let the library handle loading and validation.

Key benefits:

  1. Load configuration from environment variables.

  2. Validate configuration values using Python types.

  3. Support .env files.

  4. Organize application settings in one place.

  5. Reduce hardcoded configuration.

  6. Make applications easier to deploy and maintain.

Installation

Install the package using pip:

pip install pydantic-settings

Your First Pydantic Settings Example

Let's create a simple application configuration.

import os
from pydantic_settings import BaseSettings, SettingsConfigDict

# Set environment variables for demonstration
os.environ["APP_NAME"] = "CLCODING"
os.environ["APP_DEBUG"] = "true"
os.environ["APP_PORT"] = "9000"


class Settings(BaseSettings):

    model_config = SettingsConfigDict(
        env_prefix="APP_"
    )

    name: str = "My Python App"
    debug: bool = False
    port: int = 8000


settings = Settings()

print("App:", settings.name)
print("Debug:", settings.debug)
print("Port:", settings.port)

Expected Output

App: CLCODING
Debug: True
Port: 9000

How does this code work?

1. Import the required classes

from pydantic_settings import BaseSettings, SettingsConfigDict

BaseSettings provides the settings functionality, while SettingsConfigDict lets you configure how settings are loaded.

2. Define environment variables

os.environ["APP_NAME"] = "CLCODING"
os.environ["APP_DEBUG"] = "true"
os.environ["APP_PORT"] = "9000"

These simulate environment variables that would normally be configured outside your application.

3. Create a Settings class

class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_prefix="APP_")

    name: str = "My Python App"
    debug: bool = False
    port: int = 8000

The APP_ prefix tells Pydantic Settings to look for:

Python fieldEnvironment variable
nameAPP_NAME
debugAPP_DEBUG
portAPP_PORT

4. Create the settings object

settings = Settings()

The library reads matching environment variables and validates their values against the declared types.

For example, "9000" becomes the integer 9000, and "true" becomes the boolean True.

If an environment variable is not provided, the field's default value is used.

Using a .env File

For larger projects, you may prefer to keep configuration in a .env file during local development.

Create a file named .env:

APP_NAME=CLCODING
APP_DEBUG=true
APP_PORT=9000

Now update your Python code:

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):

    model_config = SettingsConfigDict(
        env_prefix="APP_",
        env_file=".env",
        env_file_encoding="utf-8"
    )

    name: str = "My Python App"
    debug: bool = False
    port: int = 8000


settings = Settings()

print(settings.model_dump())

Expected Output

{
    'name': 'CLCODING',
    'debug': True,
    'port': 9000
}

Important: Install python-dotenv when using dotenv file support:

pip install python-dotenv

Environment variables take priority over values loaded from a .env file. Keep real credentials out of source control and add .env to your .gitignore.

Practical Example: Database Configuration

You can also use Pydantic Settings to manage database configuration.

from pydantic_settings import BaseSettings, SettingsConfigDict


class DatabaseSettings(BaseSettings):

    model_config = SettingsConfigDict(
        env_prefix="DB_"
    )

    host: str = "localhost"
    port: int = 5432
    name: str = "my_database"
    user: str = "admin"


db = DatabaseSettings()

print("Database Host:", db.host)
print("Database Port:", db.port)
print("Database Name:", db.name)
print("Database User:", db.user)

You can override these values through environment variables without changing your Python source code.

For example:

DB_HOST=production.example.com
DB_PORT=5432
DB_NAME=production_db
DB_USER=app_user

This makes it easier to use the same application code across multiple environments.

Where Can You Use Pydantic Settings?

Pydantic Settings is particularly useful for:

1. FastAPI Applications

Manage application configuration, database connections, and external service settings.

2. Machine Learning Projects

Configure model paths, experiment parameters, API endpoints, and storage locations.

3. AI Applications

Manage LLM provider settings, API endpoints, model names, and service configuration.

4. Backend Development

Separate application configuration from business logic.

5. Production Deployment

Provide environment-specific configuration without changing application source code.

Best Practices

  • Avoid hardcoding API keys and passwords.

  • Use environment variables for deployment configuration.

  • Keep local .env files out of Git.

  • Define appropriate Python types for settings.

  • Use sensible defaults only for non-sensitive configuration.

  • Validate required settings instead of silently accepting missing values.

  • Use a secrets manager for sensitive production credentials.

Remember: Pydantic Settings helps load and validate configuration. It does not automatically encrypt or secure your secrets.

Conclusion

Configuration management is an essential part of professional Python development.

pydantic-settings makes this process easier by combining environment variable loading, type validation, and structured configuration into a simple Python interface.

If you are building APIs, backend services, machine learning applications, or AI-powered tools, this library is worth learning.

Try it in your next Python project!

Useful Resources

Follow CLCODING for more Python tutorials, libraries, coding tips, and practical developer resources.

From Code to Light: A Beautiful Diwali Diya Animation in Python

 


from __future__ import annotations

import math import time from dataclasses import dataclass from typing import Optional, Tuple import numpy as np TAU = 2.0 * math.pi # ============================================================================= # 1. CONFIGURATION (everything here can also be set from the command line) # ============================================================================= @dataclass class Config: fps: float = 30.0 # target frames per second speed: float = 1.0 # global animation speed (also speeds up the intro) intro: float = 1.0 # multiplies the length of the intro stages (1-4) density: float = 1.0 # particle density multiplier glow: float = 1.0 # strength of the surrounding ambient glow accent: float = 1.0 # amount of cyan / electric-blue accents (0 = none) cols: int = 0 # canvas width in characters (0 = whole terminal) rows: int = 0 # canvas height in lines (0 = whole terminal) color_mode: str = "auto" # auto | truecolor | 256 seed: Optional[int] = None # fixed random seed for a repeatable animation duration: float = 0.0 # stop automatically after N seconds (0 = never) # Colour ramps: (heat 0..1, (r, g, b)). Edit these to re-colour the whole piece. FIRE_STOPS = [ (0.00, (35, 4, 28)), # deep plum (0.12, (125, 8, 38)), # ruby red (0.27, (220, 30, 42)), # vivid crimson (0.42, (255, 76, 35)), # coral (0.58, (255, 137, 25)), # bright orange (0.73, (255, 194, 45)), # amber (0.88, (255, 235, 105)), # golden yellow (1.00, (255, 255, 230)), # warm-white core ] ACCENT_STOPS = [ (0.00, (35, 12, 110)), # royal purple (0.24, (88, 35, 210)), # violet (0.48, (0, 115, 255)), # electric blue (0.70, (0, 220, 220)), # turquoise (0.86, (70, 255, 170)), # emerald (1.00, (235, 255, 255)), # icy highlight ] FIRE_STEPS = 48 # number of distinct fire colours ACCENT_STEPS = 20 # number of distinct accent colours # Glyphs. Index into GLYPHS is what the frame buffers store (0 = blank). GLYPHS = " .,:;!|/\\-_=+*xX#%@" LEVEL_GROUPS = [".,", ":;", "+*", "xX#", "%@"] # very low .. maximum intensity LEVEL_EDGES = np.array([0.07, 0.20, 0.36, 0.58, 0.80]) # heat thresholds between groups # ============================================================================= # 2. SMALL HELPERS # ============================================================================= def smoothstep(e0, e1, x): t = np.clip((x - e0) / (e1 - e0), 0.0, 1.0) return t * t * (3.0 - 2.0 * t) def flow(a, b, t): """Cheap smooth pseudo-noise in [-1, 1] built from travelling sine waves.""" return (np.sin(a * 1.7 + b * 1.1 + t * 1.9) + np.sin(a * 3.3 - b * 2.1 - t * 2.7 + 1.3) + np.sin(a * 5.9 + b * 3.7 + t * 3.9 + 2.1)) * (1.0 / 3.0) def build_ramp(stops, steps): pos = np.array([s[0] for s in stops], dtype=np.float64) rgb = np.array([s[1] for s in stops], dtype=np.float64) xs = np.linspace(0.0, 1.0, steps) cols = [np.interp(xs, pos, rgb[:, c]) for c in range(3)] return np.stack(cols, axis=1).round().astype(np.uint8) def rgb_to_xterm256(r: int, g: int, b: int) -> int: """Nearest xterm-256 colour (6x6x6 cube or greyscale ramp).""" levels = (0, 95, 135, 175, 215, 255) def nearest(v): return min(range(6), key=lambda i: abs(levels[i] - v)) ri, gi_, bi = nearest(r), nearest(g), nearest(b) cube = (levels[ri], levels[gi_], levels[bi]) cube_idx = 16 + 36 * ri + 6 * gi_ + bi gray_i = min(23, max(0, int(round((((r + g + b) / 3.0) - 8) / 10.0)))) gv = 8 + 10 * gray_i d_cube = sum((a - c) ** 2 for a, c in zip((r, g, b), cube)) d_gray = sum((a - gv) ** 2 for a in (r, g, b)) return 232 + gray_i if d_gray < d_cube else cube_idx FIRE_RAMP = build_ramp(FIRE_STOPS, FIRE_STEPS) ACCENT_RAMP = build_ramp(ACCENT_STOPS, ACCENT_STEPS) COLOR_TABLE = np.vstack([np.zeros((1, 3), np.uint8), FIRE_RAMP, ACCENT_RAMP]) ACCENT_BASE = 1 + FIRE_STEPS # first accent index in COLOR_TABLE def _glyph(c: str) -> int: return GLYPHS.index(c) GROUP_IDX = np.array( [[_glyph(g[i % len(g)]) for i in range(4)] for g in LEVEL_GROUPS], dtype=np.int32 ) # shape (5 intensity levels, 4 interchangeable variants) def shade(h, k, accent): """Heat (float), variant (int 0-3) and accent flag arrays -> (glyph, colour) index arrays.""" lvl = np.searchsorted(LEVEL_EDGES, h, side="right") # 0 = blank, 1..5 glyph = np.where(lvl > 0, GROUP_IDX[np.maximum(lvl - 1, 0), k], 0) hc = np.clip(h, 0.0, 1.0) fire = 1 + (hc * (FIRE_STEPS - 1) + 0.5).astype(np.int32) acc = ACCENT_BASE + (np.clip(h * 1.15, 0.0, 1.0) * (ACCENT_STEPS - 1) + 0.5).astype(np.int32) col = np.where(accent, acc, fire) return glyph.astype(np.uint8), col.astype(np.uint8) # ============================================================================= # 4. DIYA GEOMETRY # ============================================================================= class DiyaShape: """ Static geometry of the lamp, evaluated on the character grid. Coordinates are "normalised": X right, Y up, 1.0 = half the canvas height, and one terminal column counts as half a row so shapes keep their true proportions. """ Y_RIM = -0.20 # height of the rim ellipse centre R = 0.78 # half width of the bowl D = 0.46 # depth of the bowl below the rim P = 2.10 # super-ellipse power of the bowl profile RIM_B = 0.10 # half height of the rim ellipse (seen slightly from above) FOOT_TOP = -0.64 FOOT_BOT = -0.77 Y_WICK = Y_RIM + 0.02 def __init__(self, W: int, H: int, rng: np.random.Generator): self.W, self.H = W, H self.unit = max(4.0, min(H / 2.0, W / 4.0)) * 0.97 self.cx = (W - 1) / 2.0 self.cy = (H - 1) / 2.0 xs = (np.arange(W) - self.cx) * 0.5 / self.unit ys = (self.cy - np.arange(H)) / self.unit self.X, self.Y = np.meshgrid(xs, ys) # per-cell random constants (visibility, glyph variants, twinkle phases ...) self.r1 = rng.random((H, W)) self.r2 = rng.random((H, W)) self.r3 = rng.random((H, W)) self.phase = rng.uniform(0.0, TAU, (H, W)) self.rate = rng.uniform(1.5, 7.0, (H, W)) # glyph substitution speed self.rate2 = rng.uniform(0.7, 3.2, (H, W)) # twinkle speed inside, heat, oil, _ = self.evaluate(self.X, self.Y) self.bowl_in, self.oil = inside, oil self._build_edges(inside) # outline cells are drawn dark red / dark orange, the rest keeps its lighting self.bowl_heat = np.where(self.edge, 0.24 + 0.36 * heat, heat) * inside # soft radial illumination around the flame self.glow = np.exp(-((self.X / 0.80) ** 2 + ((self.Y - 0.10) / 0.85) ** 2)) # how late each cell is revealed while the diya forms (centre first, ragged edge) dist = np.hypot(self.X * 0.85, self.Y + 0.15) / 1.05 self.rv = 0.65 * np.clip(dist, 0.0, 1.3) + 0.35 * self.r1 # ---- analytic description of the lamp ------------------------------ def evaluate(self, X, Y): """Return (inside, static heat, oil mask, sampling weight) for arbitrary coordinates.""" R, yr = self.R, self.Y_RIM ax = np.abs(X) below = Y <= yr g = (ax / R) ** self.P + (np.maximum(yr - Y, 0.0) / self.D) ** 2 body = below & (g <= 1.0) ell = (ax / (0.98 * R)) ** 1.8 + ((Y - yr) / self.RIM_B) ** 2 rim = ell <= 1.0 foot_t = np.clip((self.FOOT_TOP - Y) / (self.FOOT_TOP - self.FOOT_BOT), 0.0, 1.0) foot_hw = 0.22 + 0.15 * foot_t ** 1.6 foot = (Y <= self.FOOT_TOP) & (Y >= self.FOOT_BOT) & (ax <= foot_hw) inside = body | rim | foot # lighting of the bowl body: brighter near the rim, highlights on the shoulders ybot = yr - self.D v = np.clip((Y - ybot) / self.D, 0.0, 1.0) heat = 0.40 + 0.34 * v ** 1.2 + 0.12 * v * np.exp(-(X / 0.5) ** 2) hl1 = np.exp(-(((X + 0.42 * R) / 0.22) ** 2 + ((Y - (yr - 0.14)) / 0.06) ** 2)) hl2 = np.exp(-(((X - 0.50 * R) / 0.15) ** 2 + ((Y - (yr - 0.20)) / 0.045) ** 2)) heat = heat + 0.40 * hl1 + 0.18 * hl2 # decorative band: a row of glowing pearls and a darker line above it yb = yr - 0.21 pearl = (np.abs(Y - yb) < 0.026) & (np.abs((X / 0.105) % 1.0 - 0.5) < 0.21) & (g < 0.72) heat = np.where(pearl, 0.92, heat) line = (np.abs(Y - (yb + 0.075)) < 0.011) & (g < 0.85) heat = np.where(line, heat * 0.55, heat) # foot foot_h = 0.30 + 0.28 * (1.0 - foot_t) heat = np.where(foot & ~body, foot_h, heat) # rim ellipse: bright front lip, softer back lip, glowing oil inside ring = rim & (ell > 0.60) heat = np.where(ring, np.where(below, 0.90, 0.55), heat) oil = rim & (ell <= 0.60) heat = np.where(oil, 0.80 + 0.18 * np.exp(-(X / 0.2) ** 2), heat) edge_b = np.exp(-((g - 0.93) / 0.14) ** 2) * below edge_e = np.exp(-((ell - 0.85) / 0.20) ** 2) weight = inside * (0.30 + 0.70 * np.maximum(np.maximum(edge_b, edge_e), foot * 0.6)) return inside, heat * inside, oil, weight def _build_edges(self, inside): """Outline cells and the glyph ( / \\ | - _ ) that follows the outline direction.""" pad = np.pad(inside, 1) eroded = inside & pad[:-2, 1:-1] & pad[2:, 1:-1] & pad[1:-1, :-2] & pad[1:-1, 2:] self.edge = inside & ~eroded def blur(a): p = np.pad(a, 1) return (4 * a + 2 * (p[:-2, 1:-1] + p[2:, 1:-1] + p[1:-1, :-2] + p[1:-1, 2:]) + (p[:-2, :-2] + p[:-2, 2:] + p[2:, :-2] + p[2:, 2:])) / 16.0 s = blur(blur(inside.astype(np.float64))) gy, gx = np.gradient(s) # d/drow, d/dcol th = np.arctan2(2.0 * gx, gy) % math.pi # direction of the outline in screen space glyph = np.full(inside.shape, _glyph("|"), np.uint8) top = gy > 0 # inside lies below -> upper outline horiz = (th < math.pi / 8) | (th >= 7 * math.pi / 8) slash = (th >= math.pi / 8) & (th < 3 * math.pi / 8) back = (th >= 5 * math.pi / 8) & (th < 7 * math.pi / 8) glyph[slash] = _glyph("/") glyph[back] = _glyph("\\") glyph[horiz & top] = _glyph("-") glyph[horiz & ~top] = _glyph("_") self.edge_glyph = glyph self.edge_sel = self.edge & (self.r3 < 0.88) # a few gaps keep it organic # ---- sampling / mapping -------------------------------------------- def to_cell(self, X, Y): c = np.rint(self.cx + 2.0 * self.unit * X).astype(np.int64) r = np.rint(self.cy - self.unit * Y).astype(np.int64) return r, c def sample_bowl(self, n: int, rng: np.random.Generator): """n home positions on the lamp (outline weighted) plus their static heat.""" xs, ys, hs = [], [], [] got = 0 while got < n: m = (n - got) * 4 + 64 X = rng.uniform(-self.R * 1.02, self.R * 1.02, m) Y = rng.uniform(self.FOOT_BOT, self.Y_RIM + self.RIM_B, m) _, heat, _, w = self.evaluate(X, Y) keep = rng.random(m) < w xs.append(X[keep]) ys.append(Y[keep]) hs.append(heat[keep]) got += int(keep.sum()) return np.concatenate(xs)[:n], np.concatenate(ys)[:n], np.concatenate(hs)[:n] # ============================================================================= # 5. FLAME # ============================================================================= class FireSimulation: """Procedural, continuously changing flame (shape, sway, flicker, heat).""" H0 = 0.98 # full flame height (normalised units) W0 = 0.20 # full flame half width def __init__(self, shape: DiyaShape): self.shape = shape self.y0 = shape.Y_WICK self.burst = 0.0 # set by the controller for "glowing bursts" def height(self, t, grow): f = (1.0 + 0.055 * math.sin(TAU * 1.9 * t + 0.5) + 0.040 * math.sin(TAU * 3.7 * t + 1.7) + 0.025 * math.sin(TAU * 7.3 * t + 0.2)) return self.H0 * grow * f * (1.0 + 0.10 * self.burst) def sway(self, s, t, grow): sc = np.clip(s, 0.0, 1.0) return grow * sc ** 1.5 * (0.050 * np.sin(TAU * 0.55 * t + 2.4 * sc) + 0.032 * np.sin(TAU * 1.35 * t + 4.3 * sc + 1.0) + 0.016 * np.sin(TAU * 3.10 * t + 7.0 * sc + 2.0)) def half_width(self, s, t, grow): sc = np.clip(s, 0.0, 1.0) body = np.maximum(np.sin(math.pi * sc ** 0.62), 0.0) ** 0.9 wob = 1.0 + 0.14 * flow(sc * 3.0, 0.0, t * 2.3) return np.maximum(self.W0 * grow ** 0.8 * body * wob, 0.010) @staticmethod def profile(u, s): """Flame heat for a point at |u| half-widths from the centre line, s along its height.""" sc = np.clip(s, 0.0, 1.0) body = np.exp(-(u / 0.98) ** 4) # defined flame edge halo = np.exp(-(u / 1.7) ** 2) # soft red-orange fringe core = np.exp(-(u / 0.50) ** 2) * (1.0 - 0.75 * sc) return (0.74 * body + 0.20 * halo + 0.34 * core) * (1.0 - 0.25 * sc ** 2) def place(self, s, u, t, grow): """Map flame coordinates (s along height, u across width) to normalised X, Y.""" Y = self.y0 + s * self.height(t, grow) X = self.sway(s, t, grow) + u * self.half_width(s, t, grow) return X, Y def field(self, t, grow, energy): """Flame heat (0..1) for every character cell.""" sh = self.shape X, Y = sh.X, sh.Y hf = max(self.height(t, grow), 1e-3) s = (Y - self.y0) / hf sc = np.clip(s, 0.0, 1.0) w = self.half_width(sc, t, grow) u = np.abs(X - self.sway(sc, t, grow)) / w u = u * (1.0 + 0.22 * flow(X * 6.0, Y * 6.5 - t * 2.6, t * 1.3)) heat = self.profile(u, sc) env = smoothstep(-0.03, 0.10, s) * (1.0 - smoothstep(0.93, 1.12, s)) bands = 0.88 + 0.12 * flow(X * 4.5, Y * 9.0 - t * 4.2, t * 0.7) return np.clip(heat * env * bands * energy, 0.0, 1.0) # ============================================================================= # 6. PARTICLES # ============================================================================= @dataclass class Particle: """Snapshot of one particle. ParticleSystem stores the same fields as numpy columns (struct-of-arrays) so thousands of them can be updated per frame.""" x: float = 0.0 y: float = 0.0 vx: float = 0.0 vy: float = 0.0 intensity: float = 1.0 life: float = 1.0 max_life: float = 1.0 flicker: float = 4.0 # Hz accent: bool = False char: str = " " color: Tuple[int, int, int] = (0, 0, 0) class Controls: """Everything the animation stages drive (see AnimationController.controls).""" stage = 1 gain = 0.0 # fraction of particles that are visible radius = 0.05 # size of the initial spark cloud structure = 0.0 # how far particles have moved to their place in the diya flame_grow = 0.12 # size of the flame (0..1) field_gain = 0.0 # how much of the solid, dense diya texture is revealed glow_gain = 0.0 # ambient radial glow accent_gain = 0.0 # cyan / blue accents allowed core_heat = 0.55 # temperature of the middle of the spark cloud flick_depth = 0.95 # how strongly particles blink on/off alive = False class ParticleSystem: """ Three kinds of "former" particles (bowl, flame, ambient) start as a spark cloud and drift to their place in the diya, plus a pool of free-flying sparks that rise from the flame. """ C0 = (0.0, 0.0) # centre of the first sparks (centre of the terminal) def __init__(self, shape: DiyaShape, fire: FireSimulation, cfg: Config, rng: np.random.Generator): self.shape, self.fire, self.cfg, self.rng = shape, fire, cfg, rng cells = shape.W * shape.H d = max(cfg.density, 0.05) nA, nB, nC = int(0.14 * cells * d), int(0.10 * cells * d), int(0.30 * cells * d) nS = int(0.045 * cells * d) + 24 n = nA + nB + nC self.n, self.nS = n, nS self.sl_bowl = slice(0, nA) self.sl_flame = slice(nA, nA + nB) self.sl_amb = slice(nA + nB, n) # --- common properties of the formers scale = np.array([0.75, 0.55]) self.sc_dir = np.clip(rng.standard_normal((n, 2)), -2.4, 2.4) * scale self.appear = rng.random(n) self.delay = rng.random(n) self.intensity = 0.55 + 0.45 * rng.random(n) self.flick = rng.uniform(1.5, 9.0, n) self.phase = rng.uniform(0.0, TAU, n) self.accent = np.zeros(n, bool) # --- bowl particles: sampled on the lamp self.bowl_hx, self.bowl_hy, self.bowl_heat = shape.sample_bowl(nA, rng) self.accent[self.sl_bowl] = rng.random(nA) < 0.02 * cfg.accent # --- flame particles: coordinates inside the (moving) flame s = np.empty(0) while s.size < nB: c = rng.random(nB * 3 + 32) keep = rng.random(c.size) < np.sqrt(np.maximum(np.sin(math.pi * c ** 0.62), 0.0)) s = np.concatenate([s, c[keep]]) self.fs = s[:nB] self.fu = np.clip(rng.normal(0.0, 0.55, nB), -1.5, 1.5) self.accent[self.sl_flame] = (np.abs(self.fu) > 1.0) & (rng.random(nB) < 0.35 * cfg.accent) # --- ambient particles: a halo around the flame with different behaviours r = np.abs(rng.standard_normal(nC)) * 0.44 a = rng.uniform(0.0, TAU, nC) self.amb_hx = r * np.cos(a) * 1.35 self.amb_hy = 0.06 + r * np.sin(a) * 0.95 self.beh = rng.integers(0, 4, nC) # 0 still, 1 drifts sideways, 2 rises, 3 pulses self.beh_rate = rng.uniform(0.08, 0.35, nC) self.accent[self.sl_amb] = rng.random(nC) < 0.09 * cfg.accent self._hx = np.empty(n) self._hy = np.empty(n) self._home = np.empty(n) self._hx[self.sl_bowl] = self.bowl_hx self._hy[self.sl_bowl] = self.bowl_hy # --- free-flying sparks self.s_x = np.zeros(nS) self.s_y = np.zeros(nS) self.s_vx = np.zeros(nS) self.s_vy = np.zeros(nS) self.s_life = np.zeros(nS) self.s_max = np.ones(nS) self.s_int = np.ones(nS) self.s_acc = np.zeros(nS, bool) self.s_rate = rng.uniform(2.0, 8.0, nS) self.s_ph = rng.uniform(0.0, TAU, nS) self.spark_rate = nS / 1.8 # emissions per second at full flame # ---- sparks --------------------------------------------------------- def emit_sparks(self, count: int, t: float, grow: float, strength: float = 1.0): free = np.flatnonzero(self.s_life <= 0.0)[:count] m = free.size if m == 0: return rng = self.rng s = rng.uniform(0.05, 0.70, m) u = rng.normal(0.0, 0.55, m) x, y = self.fire.place(s, u, t, grow) life = rng.uniform(0.9, 2.8, m) * (0.8 + 0.4 * strength) self.s_x[free], self.s_y[free] = x, y self.s_vx[free] = rng.normal(0.0, 0.035 * strength, m) self.s_vy[free] = rng.uniform(0.10, 0.50, m) * strength self.s_life[free] = life self.s_max[free] = life self.s_int[free] = rng.uniform(0.75, 1.0, m) self.s_acc[free] = rng.random(m) < 0.22 * self.cfg.accent def _update_sparks(self, t, dt, ctl): self.s_life -= dt np.maximum(self.s_life, -1.0, out=self.s_life) self.s_x += (self.s_vx + 0.045 * np.sin(TAU * self.s_rate * t + self.s_ph)) * dt self.s_y += self.s_vy * dt self.s_vx *= max(0.0, 1.0 - 0.25 * dt) if ctl.stage >= 4 and ctl.flame_grow > 0.3: n = self.rng.poisson(self.spark_rate * ctl.flame_grow ** 2 * dt) if n: self.emit_sparks(int(n), t, ctl.flame_grow) def snapshot(self, i: int) -> Particle: """Return spark i as a Particle record (handy for debugging / extensions).""" life = float(self.s_life[i]) h = float(self.s_int[i] * max(life, 0.0) / self.s_max[i]) ch, col = shade(np.array([h]), np.zeros(1, np.int64), np.array([bool(self.s_acc[i])])) r, g, b = COLOR_TABLE[int(col[0])].tolist() return Particle(float(self.s_x[i]), float(self.s_y[i]), float(self.s_vx[i]), float(self.s_vy[i]), float(self.s_int[i]), life, float(self.s_max[i]), float(self.s_rate[i]), bool(self.s_acc[i]), GLYPHS[int(ch[0])], (r, g, b)) # ---- per-frame state ------------------------------------------------- def render(self, t, dt, ctl: Controls): """Advance the simulation and return per-cell (heat, glyph variant, accent) layers.""" sh = self.shape H, W = sh.H, sh.W heat_buf = np.zeros((H, W)) var_buf = np.zeros((H, W), np.int64) acc_buf = np.zeros((H, W), bool) if ctl.stage < 2: return heat_buf, var_buf, acc_buf self._update_sparks(t, dt, ctl) n = self.n k = smoothstep(0.0, 1.0, np.clip((ctl.structure - 0.6 * self.delay) / 0.4, 0.0, 1.0)) ph = self.phase # cloud ("scatter") positions and heat sx = self.C0[0] + self.sc_dir[:, 0] * ctl.radius + 0.006 * np.sin(TAU * 0.7 * t + ph) sy = self.C0[1] + self.sc_dir[:, 1] * ctl.radius + 0.006 * np.cos(TAU * 0.6 * t + ph) d = np.hypot(sx - self.C0[0], (sy - self.C0[1]) * 1.3) scat = (0.10 + ctl.core_heat * np.exp(-(d / (0.22 + 0.55 * ctl.radius)) ** 2)) * self.intensity # home positions / heat hx, hy, home = self._hx, self._hy, self._home b, f, a = self.sl_bowl, self.sl_flame, self.sl_amb wave_x = 0.010 * np.sin(TAU * 0.31 * t + 3.1 * self.bowl_hy + ph[b]) hx[b] = self.bowl_hx + wave_x hy[b] = self.bowl_hy + 0.006 * np.cos(TAU * 0.27 * t + ph[b]) home[b] = self.bowl_heat * (0.80 + 0.20 * np.sin(TAU * self.flick[b] * 0.4 * t + ph[b])) fx, fy = self.fire.place(self.fs, self.fu, t, ctl.flame_grow) hx[f], hy[f] = fx, fy home[f] = self.fire.profile(np.abs(self.fu), self.fs) * (0.80 + 0.20 * self.intensity[f]) pa, beh, br = ph[a], self.beh, self.beh_rate rise_ph = (t * br * 0.5 + pa / TAU) % 1.0 drift = np.where(beh == 1, 0.07 * np.sin(TAU * br * t + pa), 0.0) rise = np.where(beh == 2, rise_ph * 0.55, 0.0) ax = self.amb_hx + 0.014 * np.sin(TAU * 0.31 * t + 3.1 * self.amb_hy + pa) + drift ay = self.amb_hy + 0.011 * np.cos(TAU * 0.27 * t + 2.3 * self.amb_hx + pa) + rise hx[a], hy[a] = ax, ay illum = np.exp(-((ax / 0.95) ** 2 + ((ay - 0.12) / 0.90) ** 2)) mod = np.ones(a.stop - a.start) mod = np.where(beh == 2, (1.0 - rise_ph) ** 0.7 * smoothstep(0.0, 0.06, rise_ph), mod) mod = np.where(beh == 3, 0.25 + 0.75 * np.maximum(0.0, np.sin(TAU * br * t + pa)) ** 3, mod) home[a] = (0.14 + 0.52 * illum) * self.intensity[a] * mod * (0.4 + 0.6 * ctl.glow_gain) # blend cloud -> home, then apply appear-over-time and on/off flickering x = sx * (1.0 - k) + hx * k y = sy * (1.0 - k) + hy * k heat = scat * (1.0 - k) + home * k vis = np.clip((ctl.gain - self.appear) * 20.0, 0.0, 1.0) raw = 0.5 + 0.5 * np.sin(TAU * self.flick * t + ph) fl = (1.0 - ctl.flick_depth) + ctl.flick_depth * smoothstep(0.15, 0.40, raw) heat = heat * vis * fl accent = self.accent & (ctl.accent_gain > 0.5) var = ((t * self.flick * 0.35 + ph).astype(np.int64)) & 3 # sparks alive = self.s_life > 0.0 shp = np.clip(self.s_life / self.s_max, 0.0, 1.0) ** 0.85 sheat = self.s_int * shp * (0.82 + 0.18 * np.sin(TAU * self.s_rate * t + self.s_ph)) * alive svar = ((t * self.s_rate * 0.5 + self.s_ph).astype(np.int64)) & 3 X = np.concatenate([x, self.s_x]) Y = np.concatenate([y, self.s_y]) Hh = np.concatenate([heat, sheat]) V = np.concatenate([var, svar]) A = np.concatenate([accent, self.s_acc]) r, c = sh.to_cell(X, Y) ok = (Hh > LEVEL_EDGES[0]) & (r >= 0) & (r < H) & (c >= 0) & (c < W) r, c, Hh, V, A = r[ok], c[ok], Hh[ok], V[ok], A[ok] order = np.argsort(Hh) # hottest particle written last wins the cell r, c = r[order], c[order] heat_buf[r, c] = Hh[order] var_buf[r, c] = V[order] acc_buf[r, c] = A[order] return heat_buf, var_buf, acc_buf # ============================================================================= # 7. ANIMATION CONTROLLER # ============================================================================= class AnimationController: """Runs the five stages and composes flame, lamp, glow and particles into one frame.""" # end of each stage in seconds at speed 1 / intro 1: # 1 darkness, 2 first sparks, 3 expansion, 4 formation, then 5 living artwork STAGE_ENDS = (1.0, 4.0, 8.5, 15.0) def __init__(self, cfg: Config, W: int, H: int): self.cfg = cfg self.rng = np.random.default_rng(cfg.seed) self.bounds = [e * max(cfg.intro, 0.05) for e in self.STAGE_ENDS] self.burst = 0.0 self.resize(W, H) def resize(self, W: int, H: int): self.W, self.H = W, H self.shape = DiyaShape(W, H, self.rng) self.fire = FireSimulation(self.shape) self.particles = ParticleSystem(self.shape, self.fire, self.cfg, self.rng) # ---- stage logic ---------------------------------------------------- def controls(self, t: float) -> Controls: b0, b1, b2, b3 = self.bounds c = Controls() if t < b0: # stage 1: darkness return c if t < b1: # stage 2: first sparks e = smoothstep(0.0, 1.0, (t - b0) / (b1 - b0)) c.stage, c.gain = 2, 0.002 + 0.028 * e c.radius, c.core_heat = 0.05 + 0.10 * e, 0.50 + 0.12 * e elif t < b2: # stage 3: particle expansion p = (t - b1) / (b2 - b1) e = smoothstep(0.0, 1.0, p) c.stage, c.gain = 3, 0.03 + 0.72 * p ** 1.5 c.radius, c.structure = 0.15 + 0.85 * e, 0.34 * e c.core_heat = 0.62 + 0.30 * e c.glow_gain = 0.30 * smoothstep(0.4, 1.0, p) c.accent_gain, c.flick_depth = p, 0.95 - 0.10 * p elif t < b3: # stage 4: diya formation p = (t - b2) / (b3 - b2) e = smoothstep(0.0, 1.0, p) c.stage, c.gain = 4, 0.75 + 0.25 * e c.radius, c.structure = 1.0, 0.34 + 0.80 * e c.flame_grow = 0.12 + 0.88 * smoothstep(0.05, 0.95, p) c.field_gain = smoothstep(0.25, 1.0, p) c.glow_gain, c.accent_gain = 0.30 + 0.70 * e, 1.0 c.core_heat, c.flick_depth = 0.95, 0.85 - 0.50 * e else: # stage 5: living artwork c.stage, c.gain, c.radius, c.structure = 5, 1.0, 1.0, 1.14 c.flame_grow, c.field_gain, c.glow_gain, c.accent_gain = 1.0, 1.0, 1.0, 1.0 c.core_heat, c.flick_depth, c.alive = 0.95, 0.35, True return c def _tick_bursts(self, t, dt, ctl): self.burst *= math.exp(-dt / 0.55) if ctl.alive and self.rng.random() < 0.30 * dt: # occasional glowing burst self.burst = 1.0 self.particles.emit_sparks(max(8, int(0.10 * self.particles.nS)), t, ctl.flame_grow, strength=1.5) self.fire.burst = self.burst # ---- composition ------------------------------------------------------ def frame(self, t: float, dt: float) -> Tuple[np.ndarray, np.ndarray]: ctl = self.controls(t) sh = self.shape if ctl.stage < 2: z = np.zeros((sh.H, sh.W), np.uint8) return z, z self._tick_bursts(t, dt, ctl) energy = 1.0 + 0.16 * self.burst flick = (0.90 + 0.05 * math.sin(TAU * 3.3 * t + 1.0) + 0.03 * math.sin(TAU * 7.9 * t) + 0.02 * math.sin(TAU * 13.1 * t + 2.0)) # solid layers: flame + lamp, revealed from the centre outwards flame = self.fire.field(t, ctl.flame_grow, energy) shimmer = 0.84 + 0.16 * np.sin(t * sh.rate + sh.phase) light = float(np.clip(flick + 0.10 * self.burst, 0.8, 1.15)) bowl = sh.bowl_heat * light * shimmer rev = np.clip((ctl.field_gain * 1.35 - sh.rv) / 0.22, 0.0, 1.0) field = np.maximum(flame, bowl) * rev field = field * (0.92 + 0.08 * np.sin(t * sh.rate * 1.3 + sh.phase * 2.0)) # ambient glow: a dithered, twinkling halo g = sh.glow * ctl.glow_gain * self.cfg.glow * (0.82 + 0.18 * flick + 0.25 * self.burst) tw = 0.5 + 0.5 * np.sin(t * sh.rate2 + sh.phase) glow_h = np.where(sh.r2 < np.clip(g * 1.25, 0.0, 1.0), g * 0.40 * (0.40 + 0.75 * tw), 0.0) field = np.maximum(field, glow_h) # digital accents: a few cyan/blue cells twinkling in the glow acc_f = (sh.r3 > 1.0 - 0.020 * self.cfg.accent) & (g > 0.10) & (tw > 0.80) & (field < 0.30) field = np.where(acc_f, 0.30 + 0.5 * tw, field) var_f = ((t * sh.rate + sh.phase * 2.0).astype(np.int64)) & 3 # particle layer on top hp, vp, ap = self.particles.render(t, dt, ctl) use_p = hp > field heat = np.where(use_p, hp, field) var = np.where(use_p, vp, var_f) acc = np.where(use_p, ap, acc_f) ch, col = shade(heat, var, acc) # outline glyphs that follow the curve of the lamp edge = sh.edge_sel & ~use_p & (rev > 0.5) & (ch != 0) ch = np.where(edge, sh.edge_glyph, ch).astype(np.uint8) return ch, col # JUPYTER NOTEBOOK RENDERER from IPython.display import display, HTML, clear_output from html import escape def frame_to_html(ch, col): rows = [] for rr in range(ch.shape[0]): pieces, run, last_color = [], [], None for cc in range(ch.shape[1]): gid = int(ch[rr, cc]) if gid == 0: char, color = " ", None else: char = GLYPHS[gid] rgb = COLOR_TABLE[int(col[rr, cc])] color = "#{:02x}{:02x}{:02x}".format(*map(int, rgb)) if color != last_color and run: txt = escape("".join(run)).replace(" ", "&nbsp;") pieces.append(txt if last_color is None else '<span style="color:{}">{}</span>'.format(last_color, txt)) run = [] run.append(char) last_color = color if run: txt = escape("".join(run)).replace(" ", "&nbsp;") pieces.append(txt if last_color is None else '<span style="color:{}">{}</span>'.format(last_color, txt)) rows.append("".join(pieces)) return "<br>".join(rows) def run_notebook(duration=25, fps=12, width=84, height=30, speed=1.0, density=0.85, glow=1.0, accent=1.0, seed=None): cfg = Config(fps=fps, speed=speed, intro=1.0, density=density, glow=glow, accent=accent, cols=width, rows=height, color_mode="truecolor", seed=seed, duration=duration) controller = AnimationController(cfg, width, height) frame_dt = 1.0 / max(1, fps) start = last = time.perf_counter() next_frame = start try: while True: now = time.perf_counter() elapsed = now - start if duration > 0 and elapsed >= duration: break dt = min(now - last, 0.1) ch, col = controller.frame(elapsed * speed, dt * speed) artwork = frame_to_html(ch, col) clear_output(wait=True) html = ('<div style="background:radial-gradient(ellipse at 50% 48%, #241044 0%, #090817 48%, #020207 100%);padding:18px 12px;border:1px solid #49306f;border-radius:16px;text-align:center;box-shadow:0 0 28px rgba(130,70,255,.22),inset 0 0 28px rgba(0,220,220,.08);">' '<div style="background:linear-gradient(90deg,#ff5d8f,#ffd166,#5ce1e6,#b58cff);-webkit-background-clip:text;color:transparent;font:bold 23px Arial;margin-bottom:12px;">HAPPY DIWALI ✨</div>' '<div style="display:inline-block;text-align:left;color:white;line-height:1.05;' 'font:13px Consolas, Courier New, monospace;white-space:nowrap;">' + artwork + '</div>' '<div style="background:linear-gradient(90deg,#ff8fab,#ffd166,#6ee7d8);-webkit-background-clip:text;color:transparent;font:bold 14px Arial;margin-top:12px;">' 'May your life shine bright! | CLCODING</div></div>') display(HTML(html)) last = now next_frame += frame_dt delay = next_frame - time.perf_counter() if delay > 0: time.sleep(delay) else: next_frame = time.perf_counter() except KeyboardInterrupt: pass finally: clear_output(wait=True) display(HTML('<h3 style="color:#e6a700">Shubh Deepavali! — CLCODING</h3>')) # Run the animation. Adjust these settings as needed. run_notebook(duration=25, fps=12, width=84, height=30)

Popular Posts

Categories

100 Python Programs for Beginner (119) AI (355) Android (25) AngularJS (1) API (2) Assembly Language (2) aws (31) Azure (12) BI (10) book (1) Books (359) Bootcamp (20) C (78) C# (12) C++ (83) cloud (1) Course (96) Coursera (306) Cybersecurity (37) data (10) Data Analysis (48) Data Analytics (31) data management (16) Data Science (442) Data Strucures (21) Deep Learning (227) Django (17) Downloads (3) edx (21) Engineering (15) Euron (30) Events (9) Excel (24) Finance (13) flask (4) flutter (1) FPL (17) Gadgets (1) Generative AI (79) Git (14) Google (55) Hadoop (3) HTML Quiz (1) HTML&CSS (48) IBM (43) IoT (3) IS (25) Java (99) Leet Code (4) Machine Learning (416) Meta (24) MICHIGAN (5) microsoft (13) Nvidia (8) Pandas (16) PHP (20) Projects (36) Python (1404) Python Bug (1) Python Coding Challenge (1301) Python Library (29) Python Mathematics (22) Python Mistakes (51) Python Pattern Challenge (36) python quixz (1) Python Quiz (679) Python Tips (115) Questions (3) R (72) React (7) Scripting (3) security (5) Selenium Webdriver (4) Software (21) SQL (56) Udemy (24) UX Research (1) web application (11) Web development (9) web scraping (3)

Followers

Python Coding for Kids ( Free Demo for Everyone)