File size: 5,029 Bytes
cfa3391
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e8a073b
 
 
 
cfa3391
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e8a073b
cfa3391
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e8a073b
 
 
 
 
 
 
 
 
 
cfa3391
e8a073b
 
 
 
 
cfa3391
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e8a073b
cfa3391
 
e8a073b
cfa3391
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
from __future__ import annotations

"""
End-to-end generative fragrance design script for PINO.

This script demonstrates a complete inverse-design workflow:
    1. Read a high-level design brief from a JSON file.
    2. Run the CMA-ES composer (src/pino/optimizer.py) to evolve a formula.
    3. Verify the winning formula through the physical/IFRA pipeline.
    4. Save the generated recipe and its 8-hour dry-down trajectory to
       data/generated_recipe_output.json.

It is intended as a reference execution profile for downstream agents and
manufacturing workflows. Run it as:

    cd /home/hermes/pino
    source .venv/bin/activate
    PYTHONPATH=src python run_generation.py --brief data/design_brief.json

The design brief JSON has the following shape:

    {
      "name": "Citrus Summer Cologne",
      "brief": "citrus summer masculine",
      "max_iterations": 50,
      "palette_size": 15,
      "allow_synthetic": false
    }
"""

import argparse
import json
import logging
from pathlib import Path
from typing import Any

from pino.ingest_formulas import (
    load_literature_manifest,
    normalize_and_unpack_recipe_from_dict,
)
from pino.optimizer import CompositionTarget, FormulationOptimizer, OptimizerConfig
from pino.verifier import FragrancePipelineVerifier

logger = logging.getLogger("run_generation")


def load_brief(path: str) -> dict[str, Any]:
    """Load a design brief from JSON."""
    with Path(path).open("r", encoding="utf-8") as f:
        return json.load(f)


def save_output(output: dict[str, Any], path: str) -> None:
    """Persist the generated recipe and trajectory to JSON."""
    Path(path).parent.mkdir(parents=True, exist_ok=True)
    with Path(path).open("w", encoding="utf-8") as f:
        json.dump(output, f, indent=2, ensure_ascii=False)
    logger.info("Saved generated recipe to %s", path)


def run(brief_path: str, output_path: str, literature_formulas: str = "data/literature_formulas.json") -> dict[str, Any]:
    """Execute the full generative design profile."""
    brief = load_brief(brief_path)
    target = CompositionTarget.from_text(brief.get("brief", ""))
    target.name = brief.get("name", target.name)

    config = OptimizerConfig(
        max_iterations=brief.get("max_iterations", 50),
        allow_synthetic=brief.get("allow_synthetic", True),
        seed=brief.get("seed", 2026),
        verbose=1,
    )

    logger.info(
        "Starting generation: brief=%r, iterations=%d, palette=%d, synthetic=%s",
        target.name,
        config.max_iterations,
        brief.get("palette_size", 20),
        config.allow_synthetic,
    )

    seed_recipe = None
    seed_id = brief.get("seed_id")
    if seed_id:
        recipes = load_literature_manifest(literature_formulas)
        recipe = next((r for r in recipes if r.get("formula_id") == seed_id), None)
        if recipe is None:
            raise ValueError(f"Seed recipe {seed_id} not found in {literature_formulas}")
        seed_recipe = normalize_and_unpack_recipe_from_dict(recipe)
        logger.info("Loaded literature seed recipe %s with %d components", seed_id, len(seed_recipe["components"]))

    optimizer = FormulationOptimizer(config)
    best = optimizer.optimize(
        target,
        palette_size=brief.get("palette_size", 20),
        seed_recipe=seed_recipe,
    )

    # Re-verify the winning formula to capture a full 8-hour trajectory.
    formula_records = best.to_formula_dict()
    verifier = FragrancePipelineVerifier()
    verification = verifier.run_sim(
        formula_records,
        duration_seconds=8 * 3600.0,
        interval_seconds=600.0,
    )

    output = {
        "design_brief": brief,
        "generated_formula": formula_records,
        "weight_fractions": {
            item["name"]: item["weight_fraction"] for item in formula_records
        },
        "status": best.status,
        "fitness": best.fitness,
        "ifra_passed": best.ifra_report.get("passed", False),
        "ifra_report": best.ifra_report,
        "prediction": best.prediction,
        "trajectory_8h": verification.get("trajectory", []),
        "message": best.message,
    }

    save_output(output, output_path)
    return output


def main() -> None:
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s %(levelname)s %(name)s: %(message)s",
    )

    parser = argparse.ArgumentParser(description="Generate a fragrance recipe from a design brief")
    parser.add_argument("--brief", default="data/design_brief.json", help="Path to design brief JSON")
    parser.add_argument("--output", default="data/generated_recipe_output.json", help="Output JSON path")
    parser.add_argument("--literature-formulas", default="data/literature_formulas.json", help="Path to literature formula manifest")
    args = parser.parse_args()

    result = run(args.brief, args.output, literature_formulas=args.literature_formulas)
    print(json.dumps(result["generated_formula"], indent=2, ensure_ascii=False))


if __name__ == "__main__":
    main()