From f8457f86e2622e69a42604fd817a5c7741a30d62 Mon Sep 17 00:00:00 2001
From: lakshay6666 <95127268+lakshay6666@users.noreply.github.com>
Date: Wed, 3 Jun 2026 20:01:43 +0200
Subject: [PATCH] Add noise generation service
---
.../service-noise-generation-service.yml | 105 +++
README.md | 2 +
services/noise-generation-service/Dockerfile | 17 +
services/noise-generation-service/Makefile | 31 +
services/noise-generation-service/README.md | 91 ++
.../noise-generation-service.openapi.json | 297 +++++++
.../noise-generation-service/pyproject.toml | 41 +
services/noise-generation-service/src/api.py | 44 +
.../src/generate_openapi.py | 22 +
.../src/noise_generation_service/__init__.py | 1 +
.../src/noise_generation_service/cli.py | 82 ++
.../src/noise_generation_service/generator.py | 47 +
.../src/noise_generation_service/rendering.py | 121 +++
.../noise-generation-service/test/test_api.py | 42 +
.../noise-generation-service/test/test_cli.py | 109 +++
.../test/test_generate_openapi.py | 17 +
.../test/test_generator.py | 20 +
services/noise-generation-service/uv.lock | 825 ++++++++++++++++++
18 files changed, 1914 insertions(+)
create mode 100644 .github/workflows/service-noise-generation-service.yml
create mode 100644 services/noise-generation-service/Dockerfile
create mode 100644 services/noise-generation-service/Makefile
create mode 100644 services/noise-generation-service/README.md
create mode 100644 services/noise-generation-service/noise-generation-service.openapi.json
create mode 100644 services/noise-generation-service/pyproject.toml
create mode 100644 services/noise-generation-service/src/api.py
create mode 100644 services/noise-generation-service/src/generate_openapi.py
create mode 100644 services/noise-generation-service/src/noise_generation_service/__init__.py
create mode 100644 services/noise-generation-service/src/noise_generation_service/cli.py
create mode 100644 services/noise-generation-service/src/noise_generation_service/generator.py
create mode 100644 services/noise-generation-service/src/noise_generation_service/rendering.py
create mode 100644 services/noise-generation-service/test/test_api.py
create mode 100644 services/noise-generation-service/test/test_cli.py
create mode 100644 services/noise-generation-service/test/test_generate_openapi.py
create mode 100644 services/noise-generation-service/test/test_generator.py
create mode 100644 services/noise-generation-service/uv.lock
diff --git a/.github/workflows/service-noise-generation-service.yml b/.github/workflows/service-noise-generation-service.yml
new file mode 100644
index 0000000..27cff2f
--- /dev/null
+++ b/.github/workflows/service-noise-generation-service.yml
@@ -0,0 +1,105 @@
+name: noise-generation-service
+
+on:
+ push:
+ branches: ["master"]
+ paths:
+ - "services/noise-generation-service/**"
+ pull_request:
+ branches: ["master"]
+ paths:
+ - "services/noise-generation-service/**"
+
+defaults:
+ run:
+ working-directory: services/noise-generation-service
+
+jobs:
+ lint:
+ name: Lint
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.12"
+ - uses: astral-sh/setup-uv@v5
+ - name: Install dependencies
+ run: uv sync
+ - name: Lint
+ run: make lint
+
+ test:
+ name: Test
+ runs-on: ubuntu-latest
+ needs: lint
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.12"
+ - uses: astral-sh/setup-uv@v5
+ - name: Install dependencies
+ run: uv sync
+ - name: Run tests
+ run: make test
+ - name: Upload coverage to Codecov
+ uses: codecov/codecov-action@v4
+ with:
+ files: services/noise-generation-service/coverage.xml
+ flags: noise-generation-service
+ token: ${{ secrets.CODECOV_TOKEN }}
+ fail_ci_if_error: false
+ if: always()
+
+ generate-openapi:
+ name: Generate OpenAPI Spec
+ runs-on: ubuntu-latest
+ needs: test
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.12"
+ - uses: astral-sh/setup-uv@v5
+ - name: Install dependencies
+ run: uv sync
+ - name: Generate OpenAPI spec
+ run: make generate-openapi
+ - name: Upload OpenAPI spec
+ uses: actions/upload-artifact@v4
+ with:
+ name: openapi-spec-noise-generation-service
+ path: services/noise-generation-service/noise-generation-service.openapi.json
+
+ build:
+ name: Build & Push Docker Image
+ runs-on: ubuntu-latest
+ needs: generate-openapi
+ if: github.event_name == 'push' && github.ref == 'refs/heads/master'
+ permissions:
+ contents: read
+ packages: write
+ steps:
+ - uses: actions/checkout@v4
+ - name: Log in to GitHub Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Extract Docker metadata
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ghcr.io/${{ github.repository_owner }}/noise-generation-service
+ tags: |
+ type=sha,prefix=sha-
+ type=raw,value=latest
+ - name: Build and push Docker image
+ uses: docker/build-push-action@v6
+ with:
+ context: services/noise-generation-service
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
diff --git a/README.md b/README.md
index a404572..6d674bc 100644
--- a/README.md
+++ b/README.md
@@ -5,6 +5,7 @@
[](https://github.com/HTW-ALADIN/ALADIN-Services/actions/workflows/service-jsonpath-mapper-service.yml)
[](https://github.com/HTW-ALADIN/ALADIN-Services/actions/workflows/service-sql-assessment-service.yml)
[](https://github.com/HTW-ALADIN/ALADIN-Services/actions/workflows/service-fermentaladin-service.yml)
+[](https://github.com/HTW-ALADIN/ALADIN-Services/actions/workflows/service-noise-generation-service.yml)
## Services
@@ -14,6 +15,7 @@
| [jsonpath-mapper-service](services/jsonpath-mapper-service/README.md) | TypeScript | [](https://github.com/HTW-ALADIN/ALADIN-Services/actions/workflows/service-jsonpath-mapper-service.yml) | [](https://codecov.io/gh/HTW-ALADIN/ALADIN-Services?flags[0]=jsonpath-mapper-service) | JSONPath Mapper — JSON-to-JSON transformation utility exposed as a Fastify HTTP API |
| [sql-assessment-service](services/sql-assessment-service/README.md) | TypeScript | [](https://github.com/HTW-ALADIN/ALADIN-Services/actions/workflows/service-sql-assessment-service.yml) | [](https://codecov.io/gh/HTW-ALADIN/ALADIN-Services?flags[0]=sql-assessment-service) | SQL Assessment — schema analysis, SQL task generation, and query grading |
| [fermentaladin-service](services/fermentaladin-service/README.md) | Python | [](https://github.com/HTW-ALADIN/ALADIN-Services/actions/workflows/service-fermentaladin-service.yml) | — | Bioreactor fermentation simulation — ODE-based multi-phase kinetic model exposed as a FastAPI HTTP service |
+| [noise-generation-service](services/noise-generation-service/README.md) | Python | [](https://github.com/HTW-ALADIN/ALADIN-Services/actions/workflows/service-noise-generation-service.yml) | [](https://codecov.io/gh/HTW-ALADIN/ALADIN-Services?flags[0]=noise-generation-service) | Noise Generation — deterministic procedural scalar fields and contour-style plots exposed as FastAPI and CLI |
## For Developers:
diff --git a/services/noise-generation-service/Dockerfile b/services/noise-generation-service/Dockerfile
new file mode 100644
index 0000000..bb30830
--- /dev/null
+++ b/services/noise-generation-service/Dockerfile
@@ -0,0 +1,17 @@
+FROM python:3.12-slim-trixie
+
+RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates \
+ && rm -rf /var/lib/apt/lists/*
+
+ADD https://astral.sh/uv/install.sh /uv-installer.sh
+RUN sh /uv-installer.sh && rm /uv-installer.sh
+
+ENV PATH="/root/.local/bin/:$PATH"
+
+COPY . /app
+WORKDIR /app
+RUN uv sync --locked --no-dev
+
+EXPOSE 8001
+
+CMD ["uv", "run", "--no-sync", "uvicorn", "api:app", "--host", "0.0.0.0", "--port", "8001", "--app-dir", "src"]
diff --git a/services/noise-generation-service/Makefile b/services/noise-generation-service/Makefile
new file mode 100644
index 0000000..2f15455
--- /dev/null
+++ b/services/noise-generation-service/Makefile
@@ -0,0 +1,31 @@
+IMAGE_NAME := noise-generation-service
+OPENAPI_OUT := noise-generation-service.openapi.json
+PORT ?= 8001
+
+.PHONY: prep build test lint start clean docker-build generate-openapi
+
+prep:
+ uv sync
+
+build:
+ @echo "No build step required for Python"
+
+test:
+ uv run pytest --cov=src --cov-report=xml --cov-report=term-missing
+
+lint:
+ uv run ruff check src/ test/
+ uv run ruff format --check src/ test/
+
+start:
+ uv run uvicorn api:app --reload --port $(PORT) --app-dir src
+
+clean:
+ rm -rf .coverage coverage.xml htmlcov/ .pytest_cache $(OPENAPI_OUT)
+ find . -type d -name __pycache__ -exec rm -rf {} +
+
+docker-build:
+ docker build -t $(IMAGE_NAME) .
+
+generate-openapi:
+ uv run python src/generate_openapi.py
diff --git a/services/noise-generation-service/README.md b/services/noise-generation-service/README.md
new file mode 100644
index 0000000..71d6bed
--- /dev/null
+++ b/services/noise-generation-service/README.md
@@ -0,0 +1,91 @@
+# Noise Generation Service
+
+HTTP and CLI wrapper for deterministic procedural noise fields used to create practice data for geo-informatics and geostatistical interpolation exercises.
+
+The service exposes scalar noise grids and contour-style SVG plots. It uses `pyfastnoiselite`, a Python wrapper around FastNoiseLite, through the adapter in `src/noise_generation_service/generator.py`.
+
+## API
+
+Start the service:
+
+```sh
+make start
+```
+
+The API listens on port `8001` by default.
+
+OpenAPI docs:
+
+```text
+http://localhost:8001/docs
+```
+
+Endpoints:
+
+| Method | Path | Description |
+| --- | --- | --- |
+| `GET` | `/health` | Liveness probe |
+| `POST` | `/noise/grid` | Return a deterministic 2D scalar grid as JSON |
+| `POST` | `/noise/contour.svg` | Return a contour-style SVG plot |
+
+Example request:
+
+```json
+{
+ "width": 80,
+ "height": 80,
+ "seed": 42,
+ "scale": 24.0,
+ "octaves": 5,
+ "persistence": 0.5,
+ "lacunarity": 2.0,
+ "levels": 10,
+ "samplePoints": 8
+}
+```
+
+## CLI
+
+The CLI mirrors the API outputs:
+
+```sh
+uv run noise-generation grid --width 80 --height 80 --seed 42 --output grid.json
+uv run noise-generation contour --width 80 --height 80 --seed 42 --output contour.svg
+```
+
+Without `--output`, the generated JSON or SVG is written to stdout.
+
+## Docker
+
+Build:
+
+```sh
+make docker-build
+```
+
+Run:
+
+```sh
+docker run --rm -p 8001:8001 noise-generation-service
+```
+
+## Hardware Requirements
+
+Minimal requirements for the default API limits:
+
+| Resource | Requirement |
+| --- | --- |
+| CPU | 1 vCPU |
+| Memory | 256 MB RAM |
+| Disk | < 200 MB image/runtime overhead, excluding Docker base layers |
+
+The default request limit is `512 x 512` grid cells. Larger grids should be guarded by deployment-level request limits or implemented as asynchronous jobs.
+
+## Development
+
+```sh
+make prep
+make lint
+make test
+make generate-openapi
+```
diff --git a/services/noise-generation-service/noise-generation-service.openapi.json b/services/noise-generation-service/noise-generation-service.openapi.json
new file mode 100644
index 0000000..a6de82f
--- /dev/null
+++ b/services/noise-generation-service/noise-generation-service.openapi.json
@@ -0,0 +1,297 @@
+{
+ "openapi": "3.1.0",
+ "info": {
+ "title": "Noise Generation Service",
+ "description": "Deterministic procedural noise API for random scalar fields and contour-style plots. Designed as a REST and CLI wrapper around an embeddable noise generator.",
+ "version": "0.1.0"
+ },
+ "paths": {
+ "/health": {
+ "get": {
+ "tags": [
+ "Health"
+ ],
+ "summary": "Health",
+ "operationId": "health_health_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "title": "Response Health Health Get"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/noise/grid": {
+ "post": {
+ "tags": [
+ "Noise"
+ ],
+ "summary": "Generate a deterministic procedural noise grid",
+ "operationId": "noise_grid_noise_grid_post",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NoiseRequest"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/GridResponse"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/noise/contour.svg": {
+ "post": {
+ "tags": [
+ "Noise"
+ ],
+ "summary": "Generate a contour-style SVG plot from a deterministic procedural noise grid",
+ "operationId": "contour_svg_noise_contour_svg_post",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/NoiseRequest"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "image/svg+xml": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "GridResponse": {
+ "properties": {
+ "width": {
+ "type": "integer",
+ "title": "Width"
+ },
+ "height": {
+ "type": "integer",
+ "title": "Height"
+ },
+ "seed": {
+ "type": "integer",
+ "title": "Seed"
+ },
+ "min": {
+ "type": "number",
+ "title": "Min"
+ },
+ "max": {
+ "type": "number",
+ "title": "Max"
+ },
+ "values": {
+ "items": {
+ "items": {
+ "type": "number"
+ },
+ "type": "array"
+ },
+ "type": "array",
+ "title": "Values",
+ "description": "Row-major normalized scalar values in the range 0..1."
+ }
+ },
+ "type": "object",
+ "required": [
+ "width",
+ "height",
+ "seed",
+ "min",
+ "max",
+ "values"
+ ],
+ "title": "GridResponse"
+ },
+ "HTTPValidationError": {
+ "properties": {
+ "detail": {
+ "items": {
+ "$ref": "#/components/schemas/ValidationError"
+ },
+ "type": "array",
+ "title": "Detail"
+ }
+ },
+ "type": "object",
+ "title": "HTTPValidationError"
+ },
+ "NoiseRequest": {
+ "properties": {
+ "width": {
+ "type": "integer",
+ "maximum": 512.0,
+ "minimum": 2.0,
+ "title": "Width",
+ "description": "Grid width in cells.",
+ "default": 128
+ },
+ "height": {
+ "type": "integer",
+ "maximum": 512.0,
+ "minimum": 2.0,
+ "title": "Height",
+ "description": "Grid height in cells.",
+ "default": 128
+ },
+ "seed": {
+ "type": "integer",
+ "maximum": 2147483647.0,
+ "minimum": 0.0,
+ "title": "Seed",
+ "description": "Deterministic random seed.",
+ "default": 0
+ },
+ "scale": {
+ "type": "number",
+ "maximum": 10000.0,
+ "exclusiveMinimum": 0.0,
+ "title": "Scale",
+ "description": "Spatial scale; larger values zoom out.",
+ "default": 32.0
+ },
+ "octaves": {
+ "type": "integer",
+ "maximum": 10.0,
+ "minimum": 1.0,
+ "title": "Octaves",
+ "description": "Number of fractal noise octaves.",
+ "default": 4
+ },
+ "persistence": {
+ "type": "number",
+ "maximum": 1.0,
+ "exclusiveMinimum": 0.0,
+ "title": "Persistence",
+ "description": "Amplitude multiplier per octave.",
+ "default": 0.5
+ },
+ "lacunarity": {
+ "type": "number",
+ "maximum": 4.0,
+ "minimum": 1.0,
+ "title": "Lacunarity",
+ "description": "Frequency multiplier per octave.",
+ "default": 2.0
+ },
+ "levels": {
+ "type": "integer",
+ "maximum": 32.0,
+ "minimum": 2.0,
+ "title": "Levels",
+ "description": "Number of color bands in SVG contour output.",
+ "default": 10
+ },
+ "samplePoints": {
+ "type": "integer",
+ "maximum": 128.0,
+ "minimum": 0.0,
+ "title": "Samplepoints",
+ "description": "Optional number of deterministic sample points to overlay in SVG output.",
+ "default": 0
+ }
+ },
+ "type": "object",
+ "title": "NoiseRequest"
+ },
+ "ValidationError": {
+ "properties": {
+ "loc": {
+ "items": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "integer"
+ }
+ ]
+ },
+ "type": "array",
+ "title": "Location"
+ },
+ "msg": {
+ "type": "string",
+ "title": "Message"
+ },
+ "type": {
+ "type": "string",
+ "title": "Error Type"
+ },
+ "input": {
+ "title": "Input"
+ },
+ "ctx": {
+ "type": "object",
+ "title": "Context"
+ }
+ },
+ "type": "object",
+ "required": [
+ "loc",
+ "msg",
+ "type"
+ ],
+ "title": "ValidationError"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/services/noise-generation-service/pyproject.toml b/services/noise-generation-service/pyproject.toml
new file mode 100644
index 0000000..615734b
--- /dev/null
+++ b/services/noise-generation-service/pyproject.toml
@@ -0,0 +1,41 @@
+[project]
+name = "noise-generation-service"
+version = "0.1.0"
+description = "FastAPI and CLI wrapper for deterministic procedural noise grids and contour-style plots"
+readme = "README.md"
+requires-python = ">=3.12"
+dependencies = [
+ "fastapi>=0.115.0",
+ "numpy>=2.2.0",
+ "pydantic>=2.10.0",
+ "pyfastnoiselite>=0.0.7",
+ "uvicorn[standard]>=0.34.0",
+]
+
+[dependency-groups]
+dev = [
+ "httpx>=0.27.0",
+ "pytest>=9.0.0",
+ "pytest-cov>=7.0.0",
+ "ruff>=0.9.0",
+]
+
+[project.scripts]
+noise-generation = "noise_generation_service.cli:main"
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/noise_generation_service"]
+
+[tool.pytest.ini_options]
+pythonpath = ["src"]
+
+[tool.ruff]
+target-version = "py312"
+line-length = 120
+
+[tool.ruff.lint]
+select = ["E", "F", "I"]
diff --git a/services/noise-generation-service/src/api.py b/services/noise-generation-service/src/api.py
new file mode 100644
index 0000000..1105afa
--- /dev/null
+++ b/services/noise-generation-service/src/api.py
@@ -0,0 +1,44 @@
+from __future__ import annotations
+
+from fastapi import FastAPI, Response
+
+from noise_generation_service.generator import NoiseRequest, generate_noise_grid
+from noise_generation_service.rendering import GridResponse, grid_to_response, render_contour_svg
+
+app = FastAPI(
+ title="Noise Generation Service",
+ description=(
+ "Deterministic procedural noise API for random scalar fields and contour-style plots. "
+ "Designed as a REST and CLI wrapper around an embeddable noise generator."
+ ),
+ version="0.1.0",
+)
+
+
+@app.get("/health", tags=["Health"])
+def health() -> dict[str, str]:
+ return {"status": "ok"}
+
+
+@app.post(
+ "/noise/grid",
+ response_model=GridResponse,
+ tags=["Noise"],
+ summary="Generate a deterministic procedural noise grid",
+)
+def noise_grid(request: NoiseRequest) -> GridResponse:
+ grid = generate_noise_grid(request)
+ return grid_to_response(grid, request)
+
+
+@app.post(
+ "/noise/contour.svg",
+ response_class=Response,
+ responses={200: {"content": {"image/svg+xml": {"schema": {"type": "string"}}}}},
+ tags=["Noise"],
+ summary="Generate a contour-style SVG plot from a deterministic procedural noise grid",
+)
+def contour_svg(request: NoiseRequest) -> Response:
+ grid = generate_noise_grid(request)
+ payload = render_contour_svg(grid, request)
+ return Response(content=payload.svg, media_type="image/svg+xml")
diff --git a/services/noise-generation-service/src/generate_openapi.py b/services/noise-generation-service/src/generate_openapi.py
new file mode 100644
index 0000000..2fbc440
--- /dev/null
+++ b/services/noise-generation-service/src/generate_openapi.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+from __future__ import annotations
+
+import json
+import pathlib
+
+from api import app
+
+_src_dir = pathlib.Path(__file__).parent
+
+
+def main() -> pathlib.Path:
+ service_root = _src_dir.parent
+ output_path = service_root / "noise-generation-service.openapi.json"
+
+ output_path.write_text(json.dumps(app.openapi(), indent=2, ensure_ascii=False))
+ print(f"OpenAPI spec written to {output_path}")
+ return output_path
+
+
+if __name__ == "__main__": # pragma: no cover
+ main()
diff --git a/services/noise-generation-service/src/noise_generation_service/__init__.py b/services/noise-generation-service/src/noise_generation_service/__init__.py
new file mode 100644
index 0000000..d641656
--- /dev/null
+++ b/services/noise-generation-service/src/noise_generation_service/__init__.py
@@ -0,0 +1 @@
+"""Noise generation service package."""
diff --git a/services/noise-generation-service/src/noise_generation_service/cli.py b/services/noise-generation-service/src/noise_generation_service/cli.py
new file mode 100644
index 0000000..d8e9639
--- /dev/null
+++ b/services/noise-generation-service/src/noise_generation_service/cli.py
@@ -0,0 +1,82 @@
+from __future__ import annotations
+
+import argparse
+import sys
+from pathlib import Path
+
+from noise_generation_service.generator import NoiseRequest, generate_noise_grid
+from noise_generation_service.rendering import grid_to_json, render_contour_svg
+
+
+def _add_common_options(parser: argparse.ArgumentParser) -> None:
+ parser.add_argument("--width", type=int, default=128)
+ parser.add_argument("--height", type=int, default=128)
+ parser.add_argument("--seed", type=int, default=0)
+ parser.add_argument("--scale", type=float, default=32.0)
+ parser.add_argument("--octaves", type=int, default=4)
+ parser.add_argument("--persistence", type=float, default=0.5)
+ parser.add_argument("--lacunarity", type=float, default=2.0)
+ parser.add_argument("--levels", type=int, default=10)
+ parser.add_argument("--sample-points", type=int, default=0)
+ parser.add_argument("-o", "--output", type=Path)
+
+
+def _request_from_args(args: argparse.Namespace) -> NoiseRequest:
+ return NoiseRequest(
+ width=args.width,
+ height=args.height,
+ seed=args.seed,
+ scale=args.scale,
+ octaves=args.octaves,
+ persistence=args.persistence,
+ lacunarity=args.lacunarity,
+ levels=args.levels,
+ samplePoints=args.sample_points,
+ )
+
+
+def _write_output(payload: str, output: Path | None) -> None:
+ if output is None:
+ sys.stdout.write(payload)
+ if not payload.endswith("\n"):
+ sys.stdout.write("\n")
+ return
+ output.write_text(payload)
+
+
+def build_parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(
+ prog="noise-generation",
+ description="Generate deterministic procedural noise grids and contour-style SVG plots.",
+ )
+ subparsers = parser.add_subparsers(dest="command", required=True)
+
+ grid_parser = subparsers.add_parser("grid", help="Write a JSON scalar grid.")
+ _add_common_options(grid_parser)
+
+ contour_parser = subparsers.add_parser("contour", help="Write a contour-style SVG plot.")
+ _add_common_options(contour_parser)
+
+ return parser
+
+
+def main(argv: list[str] | None = None) -> int:
+ parser = build_parser()
+ args = parser.parse_args(argv)
+ request = _request_from_args(args)
+ grid = generate_noise_grid(request)
+
+ if args.command == "grid":
+ _write_output(grid_to_json(grid, request), args.output)
+ return 0
+
+ if args.command == "contour":
+ _write_output(render_contour_svg(grid, request).svg, args.output)
+ return 0
+
+ parser.error(f"Unknown command: {args.command}")
+ return 2
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/services/noise-generation-service/src/noise_generation_service/generator.py b/services/noise-generation-service/src/noise_generation_service/generator.py
new file mode 100644
index 0000000..b80e702
--- /dev/null
+++ b/services/noise-generation-service/src/noise_generation_service/generator.py
@@ -0,0 +1,47 @@
+from __future__ import annotations
+
+import numpy as np
+from pydantic import BaseModel, Field
+from pyfastnoiselite import pyfastnoiselite as fastnoise
+
+
+class NoiseRequest(BaseModel):
+ width: int = Field(default=128, ge=2, le=512, description="Grid width in cells.")
+ height: int = Field(default=128, ge=2, le=512, description="Grid height in cells.")
+ seed: int = Field(default=0, ge=0, le=2**31 - 1, description="Deterministic random seed.")
+ scale: float = Field(default=32.0, gt=0.0, le=10_000.0, description="Spatial scale; larger values zoom out.")
+ octaves: int = Field(default=4, ge=1, le=10, description="Number of fractal noise octaves.")
+ persistence: float = Field(default=0.5, gt=0.0, le=1.0, description="Amplitude multiplier per octave.")
+ lacunarity: float = Field(default=2.0, ge=1.0, le=4.0, description="Frequency multiplier per octave.")
+ levels: int = Field(default=10, ge=2, le=32, description="Number of color bands in SVG contour output.")
+ samplePoints: int = Field(
+ default=0,
+ ge=0,
+ le=128,
+ description="Optional number of deterministic sample points to overlay in SVG output.",
+ )
+
+
+def generate_noise_grid(request: NoiseRequest) -> np.ndarray:
+ """Generate a normalized 2D fractal noise grid with FastNoiseLite."""
+
+ generator = fastnoise.FastNoiseLite()
+ generator.seed = request.seed
+ generator.noise_type = fastnoise.NoiseType.NoiseType_Perlin
+ generator.fractal_type = fastnoise.FractalType.FractalType_FBm
+ generator.frequency = 1.0 / request.scale
+ generator.fractal_octaves = request.octaves
+ generator.fractal_gain = request.persistence
+ generator.fractal_lacunarity = request.lacunarity
+
+ x = np.arange(request.width, dtype=np.float32)
+ y = np.arange(request.height, dtype=np.float32)
+ xx, yy = np.meshgrid(x, y)
+ coords = np.vstack((xx.ravel(), yy.ravel())).astype(np.float32)
+ grid = generator.gen_from_coords(coords).reshape((request.height, request.width)).astype(np.float64)
+
+ min_value = float(grid.min())
+ max_value = float(grid.max())
+ if max_value == min_value:
+ return np.zeros_like(grid)
+ return (grid - min_value) / (max_value - min_value)
diff --git a/services/noise-generation-service/src/noise_generation_service/rendering.py b/services/noise-generation-service/src/noise_generation_service/rendering.py
new file mode 100644
index 0000000..9b6a646
--- /dev/null
+++ b/services/noise-generation-service/src/noise_generation_service/rendering.py
@@ -0,0 +1,121 @@
+from __future__ import annotations
+
+import html
+import json
+from dataclasses import dataclass
+
+import numpy as np
+from pydantic import BaseModel, Field
+
+from noise_generation_service.generator import NoiseRequest
+
+
+class GridResponse(BaseModel):
+ width: int
+ height: int
+ seed: int
+ min: float
+ max: float
+ values: list[list[float]] = Field(description="Row-major normalized scalar values in the range 0..1.")
+
+
+class ContourSvgResponse(BaseModel):
+ svg: str
+
+
+@dataclass(frozen=True)
+class SamplePoint:
+ x: float
+ y: float
+ value: float
+
+
+_PALETTE = [
+ "#1a9f0a",
+ "#4fc20c",
+ "#9ada11",
+ "#d3e21b",
+ "#f1db2d",
+ "#edc05b",
+ "#e8a77d",
+ "#e8bca8",
+]
+
+
+def grid_to_response(grid: np.ndarray, request: NoiseRequest) -> GridResponse:
+ return GridResponse(
+ width=request.width,
+ height=request.height,
+ seed=request.seed,
+ min=float(grid.min()),
+ max=float(grid.max()),
+ values=np.round(grid, 6).tolist(),
+ )
+
+
+def grid_to_json(grid: np.ndarray, request: NoiseRequest) -> str:
+ return json.dumps(grid_to_response(grid, request).model_dump(), indent=2)
+
+
+def _color_for_value(value: float, levels: int) -> str:
+ band = min(levels - 1, max(0, int(value * levels)))
+ palette_index = round(band * (len(_PALETTE) - 1) / max(levels - 1, 1))
+ return _PALETTE[palette_index]
+
+
+def _sample_points(grid: np.ndarray, count: int, seed: int) -> list[SamplePoint]:
+ if count <= 0:
+ return []
+
+ rng = np.random.default_rng(seed)
+ height, width = grid.shape
+ xs = rng.integers(0, width, size=count)
+ ys = rng.integers(0, height, size=count)
+ return [SamplePoint(float(x), float(y), float(grid[y, x])) for x, y in zip(xs, ys)]
+
+
+def render_contour_svg(grid: np.ndarray, request: NoiseRequest) -> ContourSvgResponse:
+ cell = 4
+ plot_width = request.width * cell
+ plot_height = request.height * cell
+ samples = _sample_points(grid, request.samplePoints, request.seed)
+
+ parts = [
+ f'")
+ return ContourSvgResponse(svg="\n".join(parts))
diff --git a/services/noise-generation-service/test/test_api.py b/services/noise-generation-service/test/test_api.py
new file mode 100644
index 0000000..5f96833
--- /dev/null
+++ b/services/noise-generation-service/test/test_api.py
@@ -0,0 +1,42 @@
+from fastapi.testclient import TestClient
+
+from api import app
+
+client = TestClient(app)
+
+
+def test_health() -> None:
+ response = client.get("/health")
+ assert response.status_code == 200
+ assert response.json() == {"status": "ok"}
+
+
+def test_grid_endpoint_returns_requested_shape() -> None:
+ response = client.post("/noise/grid", json={"width": 8, "height": 6, "seed": 123})
+
+ assert response.status_code == 200
+ payload = response.json()
+ assert payload["width"] == 8
+ assert payload["height"] == 6
+ assert len(payload["values"]) == 6
+ assert len(payload["values"][0]) == 8
+
+
+def test_grid_endpoint_is_deterministic_for_seed() -> None:
+ request = {"width": 8, "height": 6, "seed": 123}
+
+ first = client.post("/noise/grid", json=request)
+ second = client.post("/noise/grid", json=request)
+
+ assert first.status_code == 200
+ assert second.status_code == 200
+ assert first.json()["values"] == second.json()["values"]
+
+
+def test_contour_endpoint_returns_svg() -> None:
+ response = client.post("/noise/contour.svg", json={"width": 8, "height": 6, "seed": 123, "samplePoints": 2})
+
+ assert response.status_code == 200
+ assert response.headers["content-type"].startswith("image/svg+xml")
+ assert "