Development#
To create a model, inherit from ModelBase and use
AstroField() to add field metadata, which can include things
like UCDs, units, and fits and IVOA keyword mappings. You can also use
some helper types like astropydantic.AstroPydanticTime. For PlantUML diagrams, you
can optionally add namespaces to your classes by adding a _namespace
attribute.
from enum import StrEnum, auto, nonmember
from typing import ClassVar
import ctao_datamodel as dm
from astropydantic import AstroPydanticTime
class AnEnumOption(StrEnum):
"""An option."""
_namespace = nonmember("Example") # PlantUML namespace
ONE = auto()
TWO = auto()
THREE = auto()
class SubModel(dm.ModelBase):
"An example sub-model."
_namespace: ClassVar = "Example.Models"
option: AnEnumOption | None = dm.AstroField(description=dm.enum_doc(AnEnumOption))
energy_min: float = dm.AstroField(
description="A value with a unit and FITS keyword mapping",
unit="TeV",
fits_keyword="E_MIN",
)
class MyModel(dm.ModelBase):
"""An example model."""
start_time: AstroPydanticTime = dm.AstroField(
description="The time",
fits_keyword="TSTART",
examples=["2025-10-02 15:23:32.12", "2025-10-02T15:23:32.12"],
)
value: float
sub: SubModel
You can inspect the model:
dm.print_model(MyModel)
Element : Type :Opt: Parent Relation
=====================================================================================
mymodel : MyModel : : none
start_time : Time : : contains
value : float : : contains
sub : SubModel : : contains
option : AnEnumOption : * : contains
energy_min : float : : contains
JSONSchema output#
You can just use the built-in Pydantic functionality to generate JSONSchema, and note that the special field metadata like unit and fits_keyword are included automatically
import json
print(json.dumps(MyModel.model_json_schema(), indent=2))
{
"$defs": {
"AnEnumOption": {
"description": "An option.",
"enum": [
"one",
"two",
"three"
],
"title": "AnEnumOption",
"type": "string"
},
"SubModel": {
"additionalProperties": false,
"description": "An example sub-model.",
"properties": {
"option": {
"anyOf": [
{
"$ref": "#/$defs/AnEnumOption"
},
{
"type": "null"
}
],
"description": "An option. Options are: \"one\", \"two\", \"three\"."
},
"energy_min": {
"description": "A value with a unit and FITS keyword mapping",
"fits_keyword": "E_MIN",
"title": "Energy Min",
"type": "number",
"unit": "TeV"
}
},
"required": [
"option",
"energy_min"
],
"title": "SubModel",
"type": "object"
}
},
"additionalProperties": false,
"description": "An example model.",
"properties": {
"start_time": {
"anyOf": [
{
"type": "string"
},
{
"format": "date-time",
"type": "string"
}
],
"description": "The time",
"examples": [
"2025-10-02 15:23:32.12",
"2025-10-02T15:23:32.12"
],
"fits_keyword": "TSTART",
"title": "Start Time"
},
"value": {
"title": "Value",
"type": "number"
},
"sub": {
"$ref": "#/$defs/SubModel"
}
},
"required": [
"start_time",
"value",
"sub"
],
"title": "MyModel",
"type": "object"
}
Model Instances#
When you construct an instance of the model, you also get some nice functions to turn it into flat metadata, for example:
m = MyModel(
start_time="2025-10-10 15:23:23.2",
value=6.2,
sub=SubModel(option="two", energy_min=10.0),
)
flat = dm.flatten_model_instance(m)
flat
{'start_time': '2025-10-10T15:23:23.200000000',
'value': 6.2,
'sub.option': 'two',
'sub.energy_min': 10.0}
dm.unflatten_model_instance(flat, MyModel)
MyModel(start_time=<Time object: scale='utc' format='isot' value=2025-10-10T15:23:23.200>, value=6.2, sub=SubModel(option=<AnEnumOption.TWO: 'two'>, energy_min=10.0))
PlantUML and LaTeX output#
You can turn this model into PlantUML diagrams and LaTeX tables:
dm.generate_latex_table_includes([MyModel, SubModel], "./generated/includes")
dm.generate_plantuml_diagrams([MyModel, SubModel], "./generated/plantuml")
That will generate:
! tree ./generated
./generated
├── includes
│ ├── table_MyModel.inc.tex
│ └── table_SubModel.inc.tex
└── plantuml
├── definitions
│ ├── Example.AnEnumOption.plantuml
│ ├── Example.Models.SubModel.plantuml
│ └── MyModel.plantuml
├── full_Example.Models.SubModel.plantuml
├── full_MyModel.plantuml
├── relations_Example.Models.SubModel.plantuml
└── relations_MyModel.plantuml
4 directories, 9 files
The latex files can be included in any latex document:
./generated/includes/table_MyModel.inc.tex:
\begin{classdef}
\caption{\texttt{MyModel}: An example model. Fields marked with ${^\oslash}$ are optional.}
\label{tab:MyModel}
\begin{tblr}{
width=\linewidth,
colspec={Q[wd=3.2cm] Q[wd=3cm] X[l]},
row{odd}={bg=moongray},
column{1-2} = {font=\scriptsize},
column{1} = {font=\ttfamily\bfseries\scriptsize},
column{3} = {font=\scriptsize},
row{1} = {bg=galaxyblue, font=\normalfont\bfseries, fg=white}
}
Name & Description & Type (Unit) \\
start\_time & The time & Time ($\mathrm{}$) \\
value & & float ($\mathrm{}$) \\
sub & & SubModel ($\mathrm{}$) \\
\end{tblr}
\end{classdef}
The plantuml outputs can be turned into diagrams using e.g.
! plantuml generated/plantuml/full_MyModel.plantuml

In a notebook, you can get a quick preview of PlantUML files using the following. There, the diagram images are not written to a file.
dm.PlantUMLDiagram(MyModel)
dm.PlantUMLDiagram.from_path("generated/plantuml/full_MyModel.plantuml")