11"""
2- Generates and saves JSON schemas for key tidy3d data structures .
2+ Generate Tidy3D JSON Schemas (docs-free, deterministic) .
33
4- This script iterates through a predefined dictionary of Tidy3D classes,
5- generates a Pydantic JSON schema for each, and saves it as a formatted
6- JSON file in the 'schemas' directory. It's designed to be run as a
7- standalone utility to update schema definitions.
4+ This utility exports JSON Schemas for key Tidy3D models and writes them into
5+ the repository `schemas/` directory, with two strict guarantees:
86
9- All are GUI supported classes.
7+ - Documentation-free: remove all "title", "description", and "units" fields at every level.
8+ - Canonicalized: deterministically sort keys and certain lists for stable output
9+ across Python versions.
10+
11+ Note: The behavior is always docs-free and canonicalized.
1012"""
1113
1214from __future__ import annotations
1315
16+ import argparse
1417import json
1518import pathlib
1619import sys
17-
18- # Attempt to import necessary classes from tidy3d.
19- try :
20- from tidy3d import (
21- EMESimulation ,
22- HeatChargeSimulation ,
23- HeatSimulation ,
24- ModeSimulation ,
25- Simulation ,
26- )
27- from tidy3d .plugins .smatrix import TerminalComponentModeler
28- except ImportError as e :
29- print (
30- f"Error: Failed to import from 'tidy3d'. Ensure it's installed. Details: { e } " ,
31- file = sys .stderr ,
32- )
33- sys .exit (1 )
34-
20+ from typing import Any
3521
3622# Define the output directory relative to this script's location.
3723# Assumes the script is in a subdirectory like 'scripts' and 'schemas' is a sibling.
38- SCHEMA_DIR = pathlib .Path (__file__ ).parent .parent / "schemas"
24+ DEFAULT_SCHEMA_DIR = pathlib .Path (__file__ ).parent .parent / "schemas"
3925
4026# Dictionary mapping a clean name to the Pydantic model class.
4127# This is the single source of truth for which schemas to export.
42- export_api_schema_dictionary = {
43- "Simulation" : Simulation ,
44- "ModeSimulation" : ModeSimulation ,
45- "EMESimulation" : EMESimulation ,
46- "HeatSimulation" : HeatSimulation ,
47- "HeatChargeSimulation" : HeatChargeSimulation ,
48- "TerminalComponentModeler" : TerminalComponentModeler ,
49- }
28+ export_api_schema_dictionary = None # populated lazily when generating
29+
30+
31+ def _stable_sort_key_for_schema_item (item : Any ) -> str :
32+ """Return a stable string key for sorting schema objects in anyOf/oneOf/allOf arrays.
33+
34+ Input is assumed to be already canonicalized and docs-free; we defensively drop
35+ top-level doc fields in case of partially processed inputs.
36+ """
37+ try :
38+ if isinstance (item , dict ):
39+ item = {k : v for k , v in item .items () if k not in {"title" , "description" , "units" }}
40+ return json .dumps (item , sort_keys = True , separators = ("," , ":" ))
41+ except Exception :
42+ return str (item )
43+
44+
45+ def _canonicalize (obj : Any ) -> Any :
46+ """Recursively canonicalize a schema object for deterministic, docs-free output.
47+
48+ Rules:
49+ - Drop all "description", "title", and "units" keys everywhere.
50+ - Sort all dict keys recursively.
51+ - Sort arrays that are order-insensitive: "required", "enum", and "type" (when list).
52+ - For "anyOf"/"oneOf"/"allOf", sort entries by a stable key after canonicalization.
53+ """
54+ if isinstance (obj , dict ):
55+ # Canonicalize nested values and drop doc keys
56+ canon : dict [str , Any ] = {}
57+ for k , v in obj .items ():
58+ if k in {"description" , "title" , "units" }:
59+ continue # drop docs
60+ canon [k ] = _canonicalize (v )
61+
62+ # Normalize known order-insensitive array fields
63+ if isinstance (canon .get ("required" ), list ):
64+ canon ["required" ] = sorted (set (canon ["required" ]))
65+ if isinstance (canon .get ("enum" ), list ):
66+ try :
67+ canon ["enum" ] = sorted (canon ["enum" ], key = lambda x : json .dumps (x , sort_keys = True ))
68+ except Exception :
69+ canon ["enum" ] = sorted (canon ["enum" ], key = str )
70+ if isinstance (canon .get ("type" ), list ):
71+ canon ["type" ] = sorted (canon ["type" ], key = str )
72+
73+ # Combination keywords: ensure stable order using canonicalized entries
74+ for key in ("anyOf" , "oneOf" , "allOf" ):
75+ if isinstance (canon .get (key ), list ):
76+ canon [key ] = sorted (canon [key ], key = _stable_sort_key_for_schema_item )
77+
78+ # Return dict with sorted keys
79+ return {k : canon [k ] for k in sorted (canon .keys ())}
80+ elif isinstance (obj , list ):
81+ return [_canonicalize (x ) for x in obj ]
82+ else :
83+ return obj
84+
85+
86+ def _load_tidy3d_models ():
87+ """Import and return the mapping of schema names to Tidy3D model classes.
88+
89+ Import is done lazily to avoid importing tidy3d when the module is merely inspected.
90+ """
91+ try :
92+ from tidy3d import (
93+ EMESimulation ,
94+ HeatChargeSimulation ,
95+ HeatSimulation ,
96+ ModeSimulation ,
97+ Simulation ,
98+ )
99+ from tidy3d .plugins .smatrix import TerminalComponentModeler
100+ except Exception as e :
101+ print (
102+ f"Error: Failed to import from 'tidy3d'. Ensure it's installed. Details: { e } " ,
103+ file = sys .stderr ,
104+ )
105+ sys .exit (1 )
106+ return {
107+ "Simulation" : Simulation ,
108+ "ModeSimulation" : ModeSimulation ,
109+ "EMESimulation" : EMESimulation ,
110+ "HeatSimulation" : HeatSimulation ,
111+ "HeatChargeSimulation" : HeatChargeSimulation ,
112+ "TerminalComponentModeler" : TerminalComponentModeler ,
113+ }
50114
51115
52- def generate_schemas ():
116+ def generate_schemas (output_dir : pathlib . Path = DEFAULT_SCHEMA_DIR ):
53117 """
54118 Generates and saves a JSON schema for each class in the global dictionary.
55119
@@ -64,19 +128,22 @@ def generate_schemas():
64128 """
65129 try :
66130 # Create the output directory if it doesn't exist.
67- SCHEMA_DIR .mkdir (parents = True , exist_ok = True )
68- print (f"Saving schemas to '{ SCHEMA_DIR } /'" )
131+ output_dir .mkdir (parents = True , exist_ok = True )
132+ print (f"Saving schemas to '{ output_dir } /'" )
69133
70- for name , class_instance in export_api_schema_dictionary .items ():
71- output_path = SCHEMA_DIR / f"{ name } .json"
134+ models = _load_tidy3d_models ()
135+ for name , class_instance in models .items ():
136+ output_path = output_dir / f"{ name } .json"
72137 print (f" -> Generating schema for '{ name } '..." )
73138
74139 # Generate the schema dictionary from the class.
75140 schema_dict = class_instance .schema ()
141+ schema_dict = _canonicalize (schema_dict )
76142
77143 # Write the schema to a file with pretty printing.
78- with open (output_path , "w" ) as f :
79- json .dump (schema_dict , f , indent = 2 )
144+ with open (output_path , "w" , encoding = "utf-8" ) as f :
145+ json .dump (schema_dict , f , indent = 2 , sort_keys = True , ensure_ascii = True )
146+ f .write ("\n " )
80147
81148 except OSError as e :
82149 print (
@@ -92,4 +159,21 @@ def generate_schemas():
92159
93160
94161if __name__ == "__main__" :
95- generate_schemas ()
162+ parser = argparse .ArgumentParser (description = "Regenerate Tidy3D JSON Schemas" )
163+ parser .add_argument (
164+ "--output-dir" ,
165+ type = pathlib .Path ,
166+ default = DEFAULT_SCHEMA_DIR ,
167+ help = "Directory to write schema JSON files (default: repo 'schemas')." ,
168+ )
169+ args = parser .parse_args ()
170+
171+ # Encourage use of a pinned Python for stable output
172+ if not (sys .version_info .major == 3 and sys .version_info .minor == 11 ):
173+ print (
174+ f"Warning: Running with Python { sys .version_info .major } .{ sys .version_info .minor } . "
175+ "For stable schema output, prefer Python 3.11 (matches CI)." ,
176+ file = sys .stderr ,
177+ )
178+
179+ generate_schemas (args .output_dir )
0 commit comments