ShukantP commited on
Commit
69c05ab
·
verified ·
1 Parent(s): 003358f

Release Cronformer v0.1.0

Browse files
MANIFEST.json ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "repo_id": "impalasys/cronformer",
3
+ "release": "v0.1.0",
4
+ "updated_unix": 1784267466,
5
+ "source_checkpoint": "/Users/shukant/Library/CloudStorage/GoogleDrive-sukantk3.4@gmail.com/My Drive/Checkpoints/Codex/Cronformer/passes/pass322-full-broad-failure-repair-v1/checkpoint/num_steps-12000",
6
+ "validation": {
7
+ "command": "bazel run //uncommonstash/cronformer:validate_hf_cronformer_release -- --hf-dir <release-dir> --local-files-only --json",
8
+ "status": "passed",
9
+ "prompt": "every night at 5pm on tuesdays and wednesdays",
10
+ "prediction": "0 17 * * 2,3"
11
+ },
12
+ "files": [
13
+ {
14
+ "name": "README.md",
15
+ "bytes": 5106,
16
+ "sha256": "3de7475af8f43e5d374d6b776c6d14e8b6c4e443b4c884f3fb00c457aa96c8ed"
17
+ },
18
+ {
19
+ "name": "RELEASE.json",
20
+ "bytes": 1129,
21
+ "sha256": "5212b2ba852f574983b0b244f21a0103f6a31b2121299981f37ceb3e3d794aaa"
22
+ },
23
+ {
24
+ "name": "__init__.py",
25
+ "bytes": 184,
26
+ "sha256": "19733b3b4e6d0cd824750e90f0be36a42d97e7a9a0b181510cf5badb9725625f"
27
+ },
28
+ {
29
+ "name": "config.json",
30
+ "bytes": 1413,
31
+ "sha256": "423c81858035bedd95eb7a369ddfc9259c0ab52dd592b45e23aca4c0b1378888"
32
+ },
33
+ {
34
+ "name": "configuration_cronformer.py",
35
+ "bytes": 2417,
36
+ "sha256": "8f000d55b8884023f4fae72cca2a75b96fd6fd5e0ffe940d1a14de4b7ab98ccd"
37
+ },
38
+ {
39
+ "name": "cron.py",
40
+ "bytes": 31551,
41
+ "sha256": "b4ec73e1899daa8d92216d590a52eef09d8fe8e9cb3ca73103170ca70d265f61"
42
+ },
43
+ {
44
+ "name": "model.safetensors",
45
+ "bytes": 25368616,
46
+ "sha256": "6c9b7a57b49c10b35fd74b21719dc593b7c262c4e4293fde3d7471853704025f"
47
+ },
48
+ {
49
+ "name": "modeling_cronformer.py",
50
+ "bytes": 15600,
51
+ "sha256": "75337ab548d435066676128fa4c48e73650d1e61a3f76bf7261a348525c41fd7"
52
+ },
53
+ {
54
+ "name": "special_tokens_map.json",
55
+ "bytes": 125,
56
+ "sha256": "b6d346be366a7d1d48332dbc9fdf3bf8960b5d879522b7799ddba59e76237ee3"
57
+ },
58
+ {
59
+ "name": "tokenizer_config.json",
60
+ "bytes": 370,
61
+ "sha256": "56043ee5aa67cf0ac3c364ac8f677ef33cd1be78081e5e3eff4a8e596daacac7"
62
+ },
63
+ {
64
+ "name": "vocab.txt",
65
+ "bytes": 231508,
66
+ "sha256": "07eced375cec144d27c900241f3e339478dec958f92fddbc551f295c992038a3"
67
+ }
68
+ ],
69
+ "model": {
70
+ "parameters": 6334826,
71
+ "model_safetensors_bytes": 25368616,
72
+ "model_safetensors_mib": 24.19,
73
+ "tensor_bytes": 25339304,
74
+ "tensor_mib": 24.17
75
+ }
76
+ }
README.md CHANGED
@@ -1,3 +1,175 @@
1
  ---
 
 
2
  license: agpl-3.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ language:
3
+ - en
4
  license: agpl-3.0
5
+ library_name: transformers
6
+ base_model: google/bert_uncased_L-2_H-128_A-2
7
+ tags:
8
+ - cron
9
+ - schedules
10
+ - text-to-cron
11
+ - structured-prediction
12
+ - custom-code
13
+ model-index:
14
+ - name: Cronformer
15
+ results:
16
+ - task:
17
+ type: text-to-cron
18
+ name: Natural Language to Cron
19
+ dataset:
20
+ type: cronformer-broad-fixture
21
+ name: Cronformer broad schedule fixture
22
+ metrics:
23
+ - type: accuracy
24
+ name: Full broad top-1 accuracy
25
+ value: 0.9961
26
+ - type: accuracy
27
+ name: Full broad top-2 accuracy
28
+ value: 0.9981
29
+ - type: accuracy
30
+ name: Full broad top-3 accuracy
31
+ value: 0.9981
32
  ---
33
+
34
+ # Cronformer
35
+
36
+ Cronformer is a compact model for converting short English schedule requests
37
+ into standard five-field cron expressions:
38
+
39
+ ```text
40
+ every night at 5pm on tuesdays and wednesdays -> 0 17 * * 2,3
41
+ ```
42
+
43
+ The model is not a text generator. It uses a small BERT encoder and a structured
44
+ decoder that predicts the cron fields directly: minute, hour, day-of-month,
45
+ month, and day-of-week.
46
+
47
+ ## Quick Start
48
+
49
+ This repository contains custom Transformers model code. Loading the model
50
+ requires `trust_remote_code=True`.
51
+
52
+ ```python
53
+ import importlib.util
54
+
55
+ import torch
56
+ from huggingface_hub import hf_hub_download
57
+ from transformers import AutoModel, AutoTokenizer
58
+
59
+ repo_id = "impalasys/cronformer"
60
+
61
+ model = AutoModel.from_pretrained(repo_id, trust_remote_code=True)
62
+ tokenizer = AutoTokenizer.from_pretrained(repo_id)
63
+
64
+ cron_py = hf_hub_download(repo_id, "cron.py")
65
+ spec = importlib.util.spec_from_file_location("cronformer_release_cron", cron_py)
66
+ cron_module = importlib.util.module_from_spec(spec)
67
+ spec.loader.exec_module(cron_module)
68
+
69
+ inputs = tokenizer(
70
+ "every night at 5pm on tuesdays and wednesdays",
71
+ return_tensors="pt",
72
+ max_length=128,
73
+ truncation=True,
74
+ padding="max_length",
75
+ )
76
+
77
+ with torch.no_grad():
78
+ output = model(**inputs)
79
+
80
+ cron = cron_module.logits_to_cron(output)[0].to_vixie_cron()
81
+ print(cron)
82
+ ```
83
+
84
+ Expected output:
85
+
86
+ ```text
87
+ 0 17 * * 2,3
88
+ ```
89
+
90
+ ## Architecture
91
+
92
+ Cronformer is an encoder-only structured prediction model for cron expressions.
93
+ It does not decode cron as text. Instead, it predicts the cron schema directly.
94
+
95
+ The model has three main parts:
96
+
97
+ 1. A compact BERT encoder, `google/bert_uncased_L-2_H-128_A-2`, encodes the
98
+ input prompt.
99
+ 2. Five learned field queries attend over the prompt representation, one for
100
+ each cron field: minute, hour, day-of-month, month, and day-of-week.
101
+ 3. Learned slot queries specialize each field state into cron decisions such as
102
+ wildcard/value/list/range/step pattern, selected values, ranges, step sizes,
103
+ nth weekday, and last-day offsets.
104
+
105
+ The output is a structured `CronOutput` object. The bundled `cron.py` helper
106
+ turns those logits into a five-field Vixie-style cron string:
107
+
108
+ ```text
109
+ minute hour day-of-month month day-of-week
110
+ ```
111
+
112
+ Cronformer does not predict seconds, years, timezone rules, holiday calendars,
113
+ or execution history.
114
+
115
+ ## Version
116
+
117
+ This is the initial public Cronformer release.
118
+
119
+ - Version: `v0.1.0`
120
+ - Architecture: field-query Cronformer
121
+ - Base encoder: `google/bert_uncased_L-2_H-128_A-2`
122
+ - Parameters: 6,334,826
123
+ - Weight file size: 25,368,616 bytes (24.19 MiB)
124
+ - Maximum input length: 128 tokens
125
+ - License: AGPL-3.0
126
+
127
+ ## Evaluation
128
+
129
+ The released checkpoint was selected from internal Cronformer development as the
130
+ best current all-around checkpoint for this architecture. The evaluation suites
131
+ below are project-specific schedule fixtures, not public benchmark datasets.
132
+
133
+ | Suite | Top-1 | Top-2 | Top-3 |
134
+ | --- | ---: | ---: | ---: |
135
+ | Sampled broad schedule fixture | 99.50% | 100.00% | 100.00% |
136
+ | Hard language fixture | 100.00% | 100.00% | 100.00% |
137
+ | Human-authored JSONL fixture | 100.00% | 100.00% | 100.00% |
138
+ | Manual smoke prompts | 100.00% | 100.00% | 100.00% |
139
+ | Full broad schedule fixture | 99.61% | 99.81% | 99.81% |
140
+
141
+ Known top-1 misses on the full broad fixture:
142
+
143
+ - `late evening at 11 PM daily`
144
+ - `every 30 minutes from 9 to 5 on Monday through Friday`
145
+
146
+ For ambiguous business-hour language, showing top-k candidates or confirming the
147
+ cron expression with the user is recommended.
148
+
149
+ ## Intended Use
150
+
151
+ Cronformer is intended for schedule-authoring interfaces, developer tools, and
152
+ automation products that need a small local model to propose cron expressions
153
+ from concise English prompts.
154
+
155
+ Good fits:
156
+
157
+ - Suggesting cron expressions in a UI.
158
+ - Ranking or displaying multiple candidate schedules.
159
+ - Local/offline cron assistance where a large LLM is unnecessary.
160
+
161
+ Poor fits:
162
+
163
+ - Legal, medical, financial, or safety-critical scheduling without review.
164
+ - Calendar-aware scheduling involving holidays, timezones, daylight saving time,
165
+ or business-specific blackout windows.
166
+ - Natural-language requests that require external state or temporal context.
167
+
168
+ ## Limitations
169
+
170
+ - English-only training/evaluation.
171
+ - Five-field cron only.
172
+ - Ambiguous prompts may have multiple valid cron interpretations.
173
+ - The model can produce syntactically valid but semantically wrong cron strings.
174
+ - Evaluation was performed on internal Cronformer fixtures.
175
+
RELEASE.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "repo_id": "impalasys/cronformer",
3
+ "release": "v0.1.0",
4
+ "source_checkpoint": "/Users/shukant/Library/CloudStorage/GoogleDrive-sukantk3.4@gmail.com/My Drive/Checkpoints/Codex/Cronformer/passes/pass322-full-broad-failure-repair-v1/checkpoint/num_steps-12000",
5
+ "release_dir": "/Users/shukant/Library/CloudStorage/GoogleDrive-sukantk3.4@gmail.com/My Drive/Checkpoints/Cronformer/hf_releases/impalasys-cronformer-v0.1.0",
6
+ "checkpoint_pass": "pass322-full-broad-failure-repair-v1",
7
+ "checkpoint_step": "num_steps-12000",
8
+ "files_expected": [
9
+ "MANIFEST.json",
10
+ "README.md",
11
+ "RELEASE.json",
12
+ "SHA256SUMS",
13
+ "__init__.py",
14
+ "config.json",
15
+ "configuration_cronformer.py",
16
+ "cron.py",
17
+ "model.safetensors",
18
+ "modeling_cronformer.py",
19
+ "special_tokens_map.json",
20
+ "tokenizer_config.json",
21
+ "vocab.txt"
22
+ ],
23
+ "validation_status": "passed",
24
+ "validation_command": "bazel run //uncommonstash/cronformer:validate_hf_cronformer_release -- --hf-dir <release-dir> --local-files-only --json",
25
+ "parameters": 6334826,
26
+ "model_safetensors_bytes": 25368616,
27
+ "model_safetensors_mib": 24.19
28
+ }
SHA256SUMS ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 3de7475af8f43e5d374d6b776c6d14e8b6c4e443b4c884f3fb00c457aa96c8ed README.md
2
+ 5212b2ba852f574983b0b244f21a0103f6a31b2121299981f37ceb3e3d794aaa RELEASE.json
3
+ 19733b3b4e6d0cd824750e90f0be36a42d97e7a9a0b181510cf5badb9725625f __init__.py
4
+ 423c81858035bedd95eb7a369ddfc9259c0ab52dd592b45e23aca4c0b1378888 config.json
5
+ 8f000d55b8884023f4fae72cca2a75b96fd6fd5e0ffe940d1a14de4b7ab98ccd configuration_cronformer.py
6
+ b4ec73e1899daa8d92216d590a52eef09d8fe8e9cb3ca73103170ca70d265f61 cron.py
7
+ 6c9b7a57b49c10b35fd74b21719dc593b7c262c4e4293fde3d7471853704025f model.safetensors
8
+ 75337ab548d435066676128fa4c48e73650d1e61a3f76bf7261a348525c41fd7 modeling_cronformer.py
9
+ b6d346be366a7d1d48332dbc9fdf3bf8960b5d879522b7799ddba59e76237ee3 special_tokens_map.json
10
+ 56043ee5aa67cf0ac3c364ac8f677ef33cd1be78081e5e3eff4a8e596daacac7 tokenizer_config.json
11
+ 07eced375cec144d27c900241f3e339478dec958f92fddbc551f295c992038a3 vocab.txt
__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # Copyright 2026 Impala Systems, Inc.
2
+ # SPDX-License-Identifier: AGPL-3.0-only
3
+
4
+ from .configuration_cronformer import CronformerConfig
5
+ from .modeling_cronformer import CronformerModel
config.json ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "CronformerModel"
4
+ ],
5
+ "auto_map": {
6
+ "AutoConfig": "configuration_cronformer.CronformerConfig",
7
+ "AutoModel": "modeling_cronformer.CronformerModel"
8
+ },
9
+ "base_model": "google/bert_uncased_L-2_H-128_A-2",
10
+ "dropout": 0.1,
11
+ "dtype": "float32",
12
+ "encoder_config": {
13
+ "attention_probs_dropout_prob": 0.1,
14
+ "hidden_act": "gelu",
15
+ "hidden_dropout_prob": 0.1,
16
+ "hidden_size": 128,
17
+ "initializer_range": 0.02,
18
+ "intermediate_size": 512,
19
+ "layer_norm_eps": 1e-12,
20
+ "max_position_embeddings": 512,
21
+ "model_type": "bert",
22
+ "num_attention_heads": 2,
23
+ "num_hidden_layers": 2,
24
+ "pad_token_id": 0,
25
+ "type_vocab_size": 2,
26
+ "vocab_size": 30522
27
+ },
28
+ "encoder_model": "google/bert_uncased_L-2_H-128_A-2",
29
+ "hidden_size": 256,
30
+ "input_sequence_length": 128,
31
+ "lang_tokenizer": "google/bert_uncased_L-2_H-128_A-2",
32
+ "library_name": "transformers",
33
+ "list_item_components": null,
34
+ "list_item_score_scale": 1.0,
35
+ "model_type": "cronformer",
36
+ "num_attention_heads": 4,
37
+ "num_field_layers": 1,
38
+ "token_value_decoding_components": [
39
+ 0,
40
+ 1
41
+ ],
42
+ "transformers_version": "4.57.1",
43
+ "use_list_item_decoding": false,
44
+ "use_list_item_scoring": false,
45
+ "use_path_score_decoding": false,
46
+ "use_token_value_decoding": false,
47
+ "use_value_count_decoding": true,
48
+ "value_count_decoding_components": [
49
+ 0,
50
+ 1,
51
+ 2,
52
+ 3,
53
+ 4
54
+ ]
55
+ }
configuration_cronformer.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Impala Systems, Inc.
2
+ # SPDX-License-Identifier: AGPL-3.0-only
3
+
4
+ from transformers import AutoConfig, PretrainedConfig
5
+
6
+
7
+ class CronformerConfig(PretrainedConfig):
8
+ model_type = "cronformer"
9
+
10
+ def __init__(
11
+ self,
12
+ encoder_model: str = "google/bert_uncased_L-2_H-128_A-2",
13
+ hidden_size: int = 256,
14
+ lang_tokenizer: str | None = None,
15
+ input_sequence_length: int = 128,
16
+ num_attention_heads: int = 4,
17
+ num_field_layers: int = 1,
18
+ dropout: float = 0.1,
19
+ use_value_count_decoding: bool = False,
20
+ value_count_decoding_components: list[int] | None = None,
21
+ use_token_value_decoding: bool = False,
22
+ token_value_decoding_components: list[int] | None = None,
23
+ use_path_score_decoding: bool = False,
24
+ use_list_item_decoding: bool = False,
25
+ use_list_item_scoring: bool = False,
26
+ list_item_score_scale: float = 1.0,
27
+ list_item_components: list[int] | None = None,
28
+ encoder_config: dict | None = None,
29
+ **kwargs,
30
+ ):
31
+ self.encoder_model = encoder_model
32
+ self.hidden_size = hidden_size
33
+ self.lang_tokenizer = lang_tokenizer or encoder_model
34
+ self.input_sequence_length = input_sequence_length
35
+ self.num_attention_heads = num_attention_heads
36
+ self.num_field_layers = num_field_layers
37
+ self.dropout = dropout
38
+ self.use_value_count_decoding = use_value_count_decoding
39
+ self.value_count_decoding_components = value_count_decoding_components or [0, 1]
40
+ self.use_token_value_decoding = use_token_value_decoding
41
+ self.token_value_decoding_components = token_value_decoding_components or [0, 1]
42
+ self.use_path_score_decoding = use_path_score_decoding
43
+ self.use_list_item_decoding = use_list_item_decoding
44
+ self.use_list_item_scoring = use_list_item_scoring
45
+ self.list_item_score_scale = list_item_score_scale
46
+ self.list_item_components = list_item_components
47
+ self.encoder_config = encoder_config
48
+ super().__init__(**kwargs)
49
+
50
+ def to_encoder_config(self):
51
+ if self.encoder_config is not None:
52
+ encoder_config = dict(self.encoder_config)
53
+ model_type = encoder_config.pop("model_type", "bert")
54
+ return AutoConfig.for_model(model_type, **encoder_config)
55
+ return AutoConfig.from_pretrained(self.encoder_model)
cron.py ADDED
@@ -0,0 +1,786 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Impala Systems, Inc.
2
+ # SPDX-License-Identifier: AGPL-3.0-only
3
+
4
+ from enum import Enum
5
+ from dataclasses import dataclass, field, is_dataclass
6
+ from typing import List, Optional, Dict, Any, TypedDict
7
+ import torch
8
+
9
+ class CronPatternType(Enum):
10
+ WILDCARD = 0
11
+ VALUE = 1
12
+ LIST = 2
13
+ RANGE = 3
14
+ STEP = 4
15
+ NTH_WEEKDAY = 5
16
+ LAST = 6
17
+ NEAREST = 7
18
+
19
+
20
+ @dataclass
21
+ class CronComponentOutput:
22
+ """Raw logits for a single component from the model."""
23
+ pattern_logits: torch.Tensor
24
+ values_logits: torch.Tensor
25
+ range_start_logits: torch.Tensor
26
+ range_end_logits: torch.Tensor
27
+ step_start_logits: torch.Tensor
28
+ step_size_logits: torch.Tensor
29
+ nth_logits: torch.Tensor
30
+ last_offset_logits: torch.Tensor
31
+ token_values_logits: Optional[torch.Tensor] = None
32
+ token_step_size_logits: Optional[torch.Tensor] = None
33
+ token_pattern_logits: Optional[torch.Tensor] = None
34
+ value_count_logits: Optional[torch.Tensor] = None
35
+ list_values_logits: Optional[torch.Tensor] = None
36
+ list_item_logits: Optional[torch.Tensor] = None
37
+ residual_adapter_selector_logits: Optional[torch.Tensor] = None
38
+ constrained_field_logits: Optional[torch.Tensor] = None
39
+ interval_scope_selector_logits: Optional[torch.Tensor] = None
40
+ use_value_count_decoding: bool = False
41
+ use_token_value_decoding: bool = False
42
+ use_path_score_decoding: bool = False
43
+ use_list_item_decoding: bool = False
44
+ use_list_item_scoring: bool = False
45
+ list_item_score_scale: float = 1.0
46
+
47
+
48
+ @dataclass
49
+ class CronOutput:
50
+ """Complete model output containing component outputs."""
51
+ minute: CronComponentOutput
52
+ hour: CronComponentOutput
53
+ dom: CronComponentOutput
54
+ month: CronComponentOutput
55
+ dow: CronComponentOutput
56
+ dom_dow_intersect_logits: torch.Tensor
57
+ semantic_binding_logits: Optional[torch.Tensor] = None
58
+
59
+ def get_component(self, name: str) -> CronComponentOutput:
60
+ return getattr(self, name)
61
+
62
+
63
+ class CronComponentDict(TypedDict):
64
+ pattern_output_ids: torch.Tensor
65
+ values_output_ids: torch.Tensor
66
+ range_start_output_ids: torch.Tensor
67
+ range_end_output_ids: torch.Tensor
68
+ step_start_output_ids: torch.Tensor
69
+ step_size_output_ids: torch.Tensor
70
+ nth_output_ids: torch.Tensor
71
+ last_offset_output_ids: torch.Tensor
72
+
73
+ class CronDict(TypedDict):
74
+ minute: CronComponentDict
75
+ hour: CronComponentDict
76
+ dom: CronComponentDict
77
+ month: CronComponentDict
78
+ dow: CronComponentDict
79
+ dom_dow_intersect_output_ids: torch.Tensor
80
+
81
+
82
+ @dataclass
83
+ class CronComponent:
84
+ type: CronPatternType
85
+ values: List[int] = field(default_factory=list)
86
+ items: List["CronComponent"] = field(default_factory=list)
87
+ step_start: Optional[int] = None
88
+ step_size: Optional[int] = None
89
+ nth: Optional[int] = None
90
+ range_start: Optional[int] = None
91
+ range_end: Optional[int] = None
92
+ last_offset: Optional[int] = None
93
+
94
+ def to_string(self, component_index: int) -> str:
95
+ if self.type == CronPatternType.WILDCARD:
96
+ return "*"
97
+ elif self.type == CronPatternType.VALUE:
98
+ if not self.values:
99
+ return "*" # Fallback
100
+ return str(self.values[0])
101
+ elif self.type == CronPatternType.LIST:
102
+ if self.items:
103
+ return ",".join(item.to_string(component_index) for item in self.items)
104
+ if not self.values:
105
+ return "*" # Fallback
106
+ return ",".join(map(str, sorted(self.values)))
107
+ elif self.type == CronPatternType.RANGE:
108
+ start = self.range_start if self.range_start is not None else 0
109
+ end = self.range_end if self.range_end is not None else self.range_start
110
+ return f"{start}-{end}"
111
+ elif self.type == CronPatternType.STEP:
112
+ step_size = self.step_size if self.step_size is not None else 1
113
+ if step_size == 1 and (self.step_start is None or self.step_start == 0):
114
+ # Typically */1
115
+ return "*"
116
+ elif self.step_start is None or self.step_start == 0:
117
+ # Typically */step
118
+ base = "*"
119
+ else:
120
+ base = str(self.step_start)
121
+ return f"{base}/{step_size}"
122
+ elif self.type == CronPatternType.NTH_WEEKDAY:
123
+ # e.g. 2#2 (Tuesday#2nd)
124
+ if not self.values:
125
+ return "*"
126
+ dow = self.values[0]
127
+ nth = self.nth if self.nth is not None else 1
128
+ return f"{dow}#{nth}"
129
+ elif self.type == CronPatternType.LAST:
130
+ # L or 5L
131
+ if self.last_offset:
132
+ return f"L-{self.last_offset}"
133
+ if not self.values:
134
+ return "L"
135
+ return f"{self.values[0]}L"
136
+ elif self.type == CronPatternType.NEAREST:
137
+ # 15W
138
+ if not self.values:
139
+ return "1W" # Fallback
140
+ return f"{self.values[0]}W"
141
+ return "*"
142
+
143
+ @dataclass
144
+ class Cron:
145
+ minute: CronComponent
146
+ hour: CronComponent
147
+ dom: CronComponent
148
+ month: CronComponent
149
+ dow: CronComponent
150
+ dom_dow_intersect: bool = True
151
+
152
+ def to_vixie_cron(self) -> str:
153
+ # TODO: Handle dom_dow_intersect
154
+ parts = [
155
+ self.minute.to_string(0),
156
+ self.hour.to_string(1),
157
+ self.dom.to_string(2),
158
+ self.month.to_string(3),
159
+ self.dow.to_string(4),
160
+ ]
161
+ return " ".join(parts)
162
+
163
+ # Pre-defined dimensions for each component
164
+ # Minute: 0-59 (60)
165
+ # Hour: 0-23 (24)
166
+ # DoM: 1-31 (32, 0 is unused/padding)
167
+ # Month: 1-12 (13, 0 is unused/padding)
168
+ # DoW: 0-6 (7)
169
+ COMPONENT_DIMS = [60, 24, 32, 13, 7]
170
+ COMPONENT_NAMES = ["minute", "hour", "dom", "month", "dow"]
171
+ MAX_LIST_ITEMS = 4
172
+ SEMANTIC_BINDING_NAMES = [
173
+ "weekday_interval_all_day",
174
+ "workday_dow_required",
175
+ "month_end_last_dom",
176
+ "office_hours_end_17",
177
+ "office_hours_end_18",
178
+ "hour_window_required",
179
+ "hour_window_absent",
180
+ ]
181
+
182
+ def _component_from_path_score(comp_output: CronComponentOutput, component_index: int, batch_index: int) -> CronComponent:
183
+ pattern_logits = comp_output.pattern_logits[batch_index]
184
+ values_logits = comp_output.values_logits[batch_index]
185
+ if comp_output.use_token_value_decoding and comp_output.token_values_logits is not None:
186
+ values_logits = comp_output.token_values_logits[batch_index].amax(dim=0)
187
+ list_values_logits = (
188
+ comp_output.list_values_logits[batch_index]
189
+ if comp_output.list_values_logits is not None
190
+ else values_logits
191
+ )
192
+ range_start_logits = comp_output.range_start_logits[batch_index]
193
+ range_end_logits = comp_output.range_end_logits[batch_index]
194
+ step_start_logits = comp_output.step_start_logits[batch_index]
195
+ step_size_logits = comp_output.step_size_logits[batch_index]
196
+ nth_logits = comp_output.nth_logits[batch_index]
197
+ last_offset_logits = comp_output.last_offset_logits[batch_index]
198
+ value_count_logits = (
199
+ comp_output.value_count_logits[batch_index]
200
+ if comp_output.value_count_logits is not None
201
+ else None
202
+ )
203
+
204
+ def list_item_score(indices: list[int]) -> torch.Tensor:
205
+ if (
206
+ not comp_output.use_list_item_scoring
207
+ or comp_output.list_item_logits is None
208
+ or not indices
209
+ ):
210
+ return torch.tensor(0.0, dtype=list_values_logits.dtype, device=list_values_logits.device)
211
+ item_logits = comp_output.list_item_logits[batch_index]
212
+ score = torch.tensor(0.0, dtype=item_logits.dtype, device=item_logits.device)
213
+ for slot, value in enumerate(sorted(indices)[: item_logits.shape[0]]):
214
+ if 0 <= value < item_logits.shape[-1]:
215
+ score = score + item_logits[slot, value]
216
+ return score * comp_output.list_item_score_scale
217
+
218
+ def list_item_indices() -> list[int]:
219
+ if not comp_output.use_list_item_decoding or comp_output.list_item_logits is None:
220
+ return []
221
+ return scoreable_list_item_indices()
222
+
223
+ def scoreable_list_item_indices() -> list[int]:
224
+ if comp_output.list_item_logits is None:
225
+ return []
226
+ item_logits = comp_output.list_item_logits[batch_index]
227
+ if comp_output.use_value_count_decoding and value_count_logits is not None:
228
+ value_count = int(torch.argmax(value_count_logits).item())
229
+ else:
230
+ value_count = int(torch.clamp(torch.round(torch.sigmoid(list_values_logits).sum()), min=1).item())
231
+ value_count = max(1, min(value_count, item_logits.shape[0]))
232
+ indices: list[int] = []
233
+ for slot in range(value_count):
234
+ ranked = torch.argsort(item_logits[slot], descending=True).flatten().tolist()
235
+ selected = next((int(value) for value in ranked if int(value) not in indices), int(ranked[0]))
236
+ indices.append(selected)
237
+ return indices
238
+
239
+ def value_component(pattern_type: CronPatternType) -> tuple[float, CronComponent]:
240
+ value_index = int(torch.argmax(values_logits).item())
241
+ return (
242
+ float((pattern_logits[pattern_type.value] + values_logits[value_index]).item()),
243
+ CronComponent(type=pattern_type, values=[value_index]),
244
+ )
245
+
246
+ argmax_pattern = CronPatternType(int(torch.argmax(pattern_logits).item()))
247
+ candidates: list[tuple[float, CronPatternType, CronComponent]] = [
248
+ (
249
+ float(pattern_logits[CronPatternType.WILDCARD.value].item()),
250
+ CronPatternType.WILDCARD,
251
+ CronComponent(type=CronPatternType.WILDCARD),
252
+ ),
253
+ ]
254
+ value_score, value_cron = value_component(CronPatternType.VALUE)
255
+ candidates.append((value_score, CronPatternType.VALUE, value_cron))
256
+
257
+ item_decoded_indices = list_item_indices()
258
+ if item_decoded_indices:
259
+ list_indices = item_decoded_indices
260
+ elif comp_output.use_value_count_decoding and value_count_logits is not None:
261
+ value_count = int(torch.argmax(value_count_logits).item())
262
+ value_count = max(1, min(value_count, list_values_logits.numel()))
263
+ list_indices = torch.topk(list_values_logits, value_count).indices.flatten().tolist()
264
+ else:
265
+ list_indices = (list_values_logits > 0.0).nonzero().flatten().tolist()
266
+ if not list_indices:
267
+ list_indices = [int(torch.argmax(list_values_logits).item())]
268
+ list_score = (
269
+ pattern_logits[CronPatternType.LIST.value]
270
+ + list_values_logits[list_indices].sum()
271
+ + list_item_score([int(value) for value in list_indices])
272
+ )
273
+ list_component = CronComponent(type=CronPatternType.LIST, values=[int(value) for value in list_indices])
274
+ last_offset_idx = int(torch.argmax(last_offset_logits).item())
275
+ if component_index == 2 and last_offset_idx == 0:
276
+ list_component.items = [
277
+ CronComponent(CronPatternType.VALUE, values=[value])
278
+ for value in sorted(list_component.values)
279
+ ]
280
+ list_component.items.append(CronComponent(CronPatternType.LAST))
281
+ candidates.append((float(list_score.item()), CronPatternType.LIST, list_component))
282
+ if comp_output.use_list_item_scoring and not comp_output.use_list_item_decoding:
283
+ scored_item_indices = scoreable_list_item_indices()
284
+ if scored_item_indices and sorted(scored_item_indices) != sorted([int(value) for value in list_indices]):
285
+ scored_item_list_score = (
286
+ pattern_logits[CronPatternType.LIST.value]
287
+ + list_values_logits[scored_item_indices].sum()
288
+ + list_item_score([int(value) for value in scored_item_indices])
289
+ )
290
+ candidates.append(
291
+ (
292
+ float(scored_item_list_score.item()),
293
+ CronPatternType.LIST,
294
+ CronComponent(
295
+ type=CronPatternType.LIST,
296
+ values=[int(value) for value in scored_item_indices],
297
+ ),
298
+ )
299
+ )
300
+
301
+ range_start = int(torch.argmax(range_start_logits).item())
302
+ range_end = int(torch.argmax(range_end_logits).item())
303
+ candidates.append(
304
+ (
305
+ float(
306
+ (
307
+ pattern_logits[CronPatternType.RANGE.value]
308
+ + range_start_logits[range_start]
309
+ + range_end_logits[range_end]
310
+ ).item()
311
+ ),
312
+ CronPatternType.RANGE,
313
+ CronComponent(type=CronPatternType.RANGE, range_start=range_start, range_end=range_end),
314
+ )
315
+ )
316
+
317
+ step_start = int(torch.argmax(step_start_logits).item())
318
+ step_size = int(torch.argmax(step_size_logits).item())
319
+ candidates.append(
320
+ (
321
+ float(
322
+ (
323
+ pattern_logits[CronPatternType.STEP.value]
324
+ + step_start_logits[step_start]
325
+ + step_size_logits[step_size]
326
+ ).item()
327
+ ),
328
+ CronPatternType.STEP,
329
+ CronComponent(type=CronPatternType.STEP, step_start=step_start, step_size=step_size),
330
+ )
331
+ )
332
+
333
+ nth_value = int(torch.argmax(values_logits).item())
334
+ nth = int(torch.argmax(nth_logits).item())
335
+ candidates.append(
336
+ (
337
+ float(
338
+ (
339
+ pattern_logits[CronPatternType.NTH_WEEKDAY.value]
340
+ + values_logits[nth_value]
341
+ + nth_logits[nth]
342
+ ).item()
343
+ ),
344
+ CronPatternType.NTH_WEEKDAY,
345
+ CronComponent(type=CronPatternType.NTH_WEEKDAY, values=[nth_value], nth=nth),
346
+ )
347
+ )
348
+
349
+ last_values = (values_logits > 0.0).nonzero().flatten().tolist()
350
+ last_value_score = torch.tensor(0.0, dtype=values_logits.dtype, device=values_logits.device)
351
+ last_component = CronComponent(type=CronPatternType.LAST)
352
+ if component_index != 2 and last_values:
353
+ last_value = int(torch.argmax(values_logits).item())
354
+ last_component.values = [last_value]
355
+ last_value_score = values_logits[last_value]
356
+ if last_offset_idx > 0:
357
+ last_component.last_offset = last_offset_idx
358
+ candidates.append(
359
+ (
360
+ float(
361
+ (
362
+ pattern_logits[CronPatternType.LAST.value]
363
+ + last_value_score
364
+ + last_offset_logits[last_offset_idx]
365
+ ).item()
366
+ ),
367
+ CronPatternType.LAST,
368
+ last_component,
369
+ )
370
+ )
371
+
372
+ nearest_value = int(torch.argmax(values_logits).item())
373
+ candidates.append(
374
+ (
375
+ float(
376
+ (
377
+ pattern_logits[CronPatternType.NEAREST.value]
378
+ + values_logits[nearest_value]
379
+ ).item()
380
+ ),
381
+ CronPatternType.NEAREST,
382
+ CronComponent(type=CronPatternType.NEAREST, values=[nearest_value]),
383
+ )
384
+ )
385
+
386
+ candidate_by_pattern = {pattern: (score, component) for score, pattern, component in candidates}
387
+ if argmax_pattern != CronPatternType.WILDCARD:
388
+ return candidate_by_pattern[argmax_pattern][1]
389
+
390
+ wildcard_score = candidate_by_pattern[CronPatternType.WILDCARD][0]
391
+ eligible = [candidate_by_pattern[CronPatternType.WILDCARD]]
392
+ range_score, range_component = candidate_by_pattern[CronPatternType.RANGE]
393
+ range_margin = 10.0
394
+ if component_index != 4 and range_score - wildcard_score >= range_margin:
395
+ eligible.append((range_score, range_component))
396
+ if component_index == 2:
397
+ last_score, last_component = candidate_by_pattern[CronPatternType.LAST]
398
+ if last_score - wildcard_score >= 6.0:
399
+ eligible.append((last_score, last_component))
400
+
401
+ return max(eligible, key=lambda item: item[0])[1]
402
+
403
+
404
+ def logits_to_cron(output: CronOutput) -> List[Cron]:
405
+ """
406
+ Converts structured CronOutput into a list of Cron objects.
407
+ """
408
+ # Assuming batch size is consistent across all tensors in output
409
+ batch_size = output.minute.pattern_logits.size(0)
410
+ crons = []
411
+
412
+ # Store component data for each item in batch
413
+ batch_components = [[] for _ in range(batch_size)]
414
+
415
+ for component_index, comp_name in enumerate(COMPONENT_NAMES):
416
+ comp_output = output.get_component(comp_name)
417
+
418
+ # Now process each item in the batch for this component
419
+ for b in range(batch_size):
420
+ if comp_output.use_path_score_decoding:
421
+ batch_components[b].append(_component_from_path_score(comp_output, component_index, b))
422
+ continue
423
+
424
+ # Extract
425
+ pattern_logits = comp_output.pattern_logits[b]
426
+ values_logits = comp_output.values_logits[b]
427
+ if comp_output.use_token_value_decoding and comp_output.token_values_logits is not None:
428
+ values_logits = comp_output.token_values_logits[b].amax(dim=0)
429
+ list_values_logits = (
430
+ comp_output.list_values_logits[b]
431
+ if comp_output.list_values_logits is not None
432
+ else values_logits
433
+ )
434
+ rs_logits = comp_output.range_start_logits[b]
435
+ re_logits = comp_output.range_end_logits[b]
436
+ step_start_logits = comp_output.step_start_logits[b]
437
+ step_size_logits = comp_output.step_size_logits[b]
438
+ nth_logits = comp_output.nth_logits[b]
439
+ value_count_logits = comp_output.value_count_logits[b] if comp_output.value_count_logits is not None else None
440
+
441
+ pattern_idx = torch.argmax(pattern_logits).item()
442
+ pattern_type = CronPatternType(pattern_idx)
443
+
444
+ # Values (Sigmoid)
445
+ values_indices = (values_logits > 0.0).nonzero().flatten().tolist()
446
+
447
+ rs_idx = torch.argmax(rs_logits).item()
448
+ re_idx = torch.argmax(re_logits).item()
449
+ step_start_idx = torch.argmax(step_start_logits).item()
450
+ step_size_idx = torch.argmax(step_size_logits).item()
451
+ nth_idx = torch.argmax(nth_logits).item()
452
+ last_offset_idx = torch.argmax(comp_output.last_offset_logits[b]).item()
453
+
454
+ # Construct Component
455
+ comp = CronComponent(type=pattern_type)
456
+
457
+ if pattern_type == CronPatternType.WILDCARD:
458
+ pass
459
+ elif pattern_type == CronPatternType.VALUE:
460
+ if not values_indices:
461
+ comp.values = [torch.argmax(values_logits).item()]
462
+ else:
463
+ # Pick the highest probability value among the candidates
464
+ best_idx = max(values_indices, key=lambda i: values_logits[i].item())
465
+ comp.values = [best_idx]
466
+ elif pattern_type == CronPatternType.LIST:
467
+ values_logits = list_values_logits
468
+ values_indices = (values_logits > 0.0).nonzero().flatten().tolist()
469
+ if comp_output.use_list_item_decoding and comp_output.list_item_logits is not None:
470
+ item_logits = comp_output.list_item_logits[b]
471
+ if comp_output.use_value_count_decoding and value_count_logits is not None:
472
+ value_count = int(torch.argmax(value_count_logits).item())
473
+ else:
474
+ value_count = int(torch.clamp(torch.round(torch.sigmoid(values_logits).sum()), min=1).item())
475
+ value_count = max(1, min(value_count, item_logits.shape[0]))
476
+ values_indices = []
477
+ for slot in range(value_count):
478
+ ranked = torch.argsort(item_logits[slot], descending=True).flatten().tolist()
479
+ selected = next((int(value) for value in ranked if int(value) not in values_indices), int(ranked[0]))
480
+ values_indices.append(selected)
481
+ elif comp_output.use_value_count_decoding and value_count_logits is not None:
482
+ value_count = int(torch.argmax(value_count_logits).item())
483
+ value_count = max(1, min(value_count, values_logits.numel()))
484
+ values_indices = torch.topk(values_logits, value_count).indices.flatten().tolist()
485
+ elif not values_indices:
486
+ values_indices = [torch.argmax(values_logits).item()]
487
+ comp.values = values_indices
488
+ if component_index == 2 and last_offset_idx == 0:
489
+ comp.items = [CronComponent(CronPatternType.VALUE, values=[value]) for value in sorted(values_indices)]
490
+ comp.items.append(CronComponent(CronPatternType.LAST))
491
+ elif pattern_type == CronPatternType.RANGE:
492
+ comp.range_start = rs_idx
493
+ comp.range_end = re_idx
494
+ elif pattern_type == CronPatternType.STEP:
495
+ comp.step_start = step_start_idx
496
+ comp.step_size = step_size_idx
497
+ elif pattern_type == CronPatternType.NTH_WEEKDAY:
498
+ comp.values = [torch.argmax(values_logits).item()]
499
+ comp.nth = nth_idx
500
+ elif pattern_type == CronPatternType.LAST:
501
+ if component_index != 2 and values_indices:
502
+ comp.values = [torch.argmax(values_logits).item()]
503
+ if last_offset_idx > 0:
504
+ comp.last_offset = last_offset_idx
505
+ elif pattern_type == CronPatternType.NEAREST:
506
+ comp.values = [torch.argmax(values_logits).item()]
507
+
508
+ batch_components[b].append(comp)
509
+
510
+ # Assemble Cron objects
511
+ for b in range(batch_size):
512
+ comps = batch_components[b] # Order matches loop above
513
+
514
+ dom_dow_logits = output.dom_dow_intersect_logits[b]
515
+ dom_dow_val = torch.argmax(dom_dow_logits).item() == 1
516
+
517
+ crons.append(Cron(
518
+ minute=comps[0],
519
+ hour=comps[1],
520
+ dom=comps[2],
521
+ month=comps[3],
522
+ dow=comps[4],
523
+ dom_dow_intersect=dom_dow_val
524
+ ))
525
+
526
+ return crons
527
+
528
+
529
+
530
+ def cron_string_to_cron(cron_str: str) -> Cron:
531
+ """
532
+ Parses a Vixie Cron string into a Cron object.
533
+ Simplistic parser for training demo.
534
+ """
535
+ parts = cron_str.split()
536
+ if len(parts) != 5:
537
+ parts = ["*", "*", "*", "*", "*"]
538
+
539
+ components_data = [
540
+ (parts[0], 0, 59), # minute
541
+ (parts[1], 0, 23), # hour
542
+ (parts[2], 1, 31), # dom
543
+ (parts[3], 1, 12), # month
544
+ (parts[4], 0, 6), # dow
545
+ ]
546
+
547
+ cron_components = []
548
+
549
+ for i, (expr, min_val, max_val) in enumerate(components_data):
550
+ comp = CronComponent(type=CronPatternType.VALUE) # Default
551
+
552
+ if expr == "*":
553
+ comp.type = CronPatternType.WILDCARD
554
+ elif "," in expr:
555
+ comp.type = CronPatternType.LIST
556
+ values = []
557
+ for val in expr.split(","):
558
+ try:
559
+ v = int(val)
560
+ values.append(v)
561
+ except: pass
562
+ comp.values = values
563
+ elif "/" in expr:
564
+ comp.type = CronPatternType.STEP
565
+ try:
566
+ base, step = expr.split("/")
567
+ if base != "*":
568
+ try:
569
+ v = int(base)
570
+ comp.step_start = v
571
+ except: pass
572
+ else:
573
+ comp.step_start = 0
574
+ comp.step_size = int(step)
575
+ except: pass
576
+ elif "#" in expr:
577
+ comp.type = CronPatternType.NTH_WEEKDAY
578
+ try:
579
+ dow, nth = map(int, expr.split("#"))
580
+ comp.values = [dow]
581
+ comp.nth = nth
582
+ except: pass
583
+ elif "L" in expr:
584
+ comp.type = CronPatternType.LAST
585
+ if expr.startswith("L-"):
586
+ try:
587
+ comp.last_offset = int(expr[2:])
588
+ except: pass
589
+ elif expr != "L":
590
+ try:
591
+ val = int(expr.replace("L", ""))
592
+ comp.values = [val]
593
+ except: pass
594
+ elif "-" in expr:
595
+ comp.type = CronPatternType.RANGE
596
+ try:
597
+ s, e = map(int, expr.split("-"))
598
+ comp.range_start = s
599
+ comp.range_end = e
600
+ except: pass
601
+ elif "W" in expr:
602
+ comp.type = CronPatternType.NEAREST
603
+ try:
604
+ val = int(expr.replace("W", ""))
605
+ comp.values = [val]
606
+ except: pass
607
+ else:
608
+ comp.type = CronPatternType.VALUE
609
+ try:
610
+ v = int(expr)
611
+ comp.values = [v]
612
+ except: pass
613
+
614
+ cron_components.append(comp)
615
+
616
+ return Cron(
617
+ minute=cron_components[0],
618
+ hour=cron_components[1],
619
+ dom=cron_components[2],
620
+ month=cron_components[3],
621
+ dow=cron_components[4],
622
+ dom_dow_intersect=True # heuristic
623
+ )
624
+
625
+ def cron_to_dict(cron: Cron) -> CronDict:
626
+ """
627
+ Converts a Cron object into a structured dictionary of tensors.
628
+ """
629
+ # Order: Minute, Hour, DoM, Month, DoW
630
+ components = [cron.minute, cron.hour, cron.dom, cron.month, cron.dow]
631
+ target_dict = {}
632
+
633
+ for i, comp in enumerate(components):
634
+ dim_size = COMPONENT_DIMS[i]
635
+ comp_name = COMPONENT_NAMES[i]
636
+
637
+ pattern_idx = comp.type.value
638
+ values_hot = [0.0] * dim_size
639
+ range_start_idx = -100
640
+ range_end_idx = -100
641
+ step_start_idx = -100
642
+ step_size_idx = -100
643
+ nth_idx = -100
644
+ last_offset_idx = -100
645
+
646
+ # Values
647
+ if comp.values:
648
+ for v in comp.values:
649
+ if 0 <= v < dim_size:
650
+ values_hot[v] = 1.0
651
+ # Range
652
+ if comp.type == CronPatternType.RANGE:
653
+ if comp.range_start is not None: range_start_idx = comp.range_start
654
+ if comp.range_end is not None: range_end_idx = comp.range_end
655
+
656
+ # Step
657
+ if comp.type == CronPatternType.STEP:
658
+ if comp.step_start is not None: step_start_idx = comp.step_start
659
+ if comp.step_size is not None: step_size_idx = comp.step_size
660
+
661
+ # Nth
662
+ if comp.type == CronPatternType.NTH_WEEKDAY:
663
+ if comp.nth is not None: nth_idx = comp.nth
664
+
665
+ # Last offset. 0 is an explicit "plain L / no offset" class for LAST.
666
+ if comp.type == CronPatternType.LAST:
667
+ last_offset_idx = comp.last_offset if comp.last_offset is not None else 0
668
+ elif comp.type == CronPatternType.LIST:
669
+ has_last_item = any(item.type == CronPatternType.LAST for item in comp.items)
670
+ if has_last_item:
671
+ last_offset_idx = 0
672
+ else:
673
+ last_offset_idx = dim_size - 1
674
+
675
+ # Create Component Dict
676
+ target_dict[comp_name] = {
677
+ "pattern_output_ids": torch.tensor(pattern_idx, dtype=torch.long),
678
+ "values_output_ids": torch.tensor(values_hot, dtype=torch.float),
679
+ "range_start_output_ids": torch.tensor(range_start_idx, dtype=torch.long),
680
+ "range_end_output_ids": torch.tensor(range_end_idx, dtype=torch.long),
681
+ "step_start_output_ids": torch.tensor(step_start_idx, dtype=torch.long),
682
+ "step_size_output_ids": torch.tensor(step_size_idx, dtype=torch.long),
683
+ "nth_output_ids": torch.tensor(nth_idx, dtype=torch.long),
684
+ "last_offset_output_ids": torch.tensor(last_offset_idx, dtype=torch.long),
685
+ }
686
+
687
+ dd_idx = 1 if cron.dom_dow_intersect else 0
688
+ target_dict["dom_dow_intersect_output_ids"] = torch.tensor(dd_idx, dtype=torch.long)
689
+
690
+ return target_dict
691
+
692
+ def parse_cron_string(cron_str: str) -> CronDict:
693
+ """
694
+ Wrapper to return target dict.
695
+ """
696
+ cron = cron_string_to_cron(cron_str)
697
+ return cron_to_dict(cron)
698
+
699
+
700
+ def parse_cron_schema(cron_schema: Dict[str, Any] | str) -> CronDict:
701
+ """
702
+ Converts a Cronformer proto JSON CronExpression into target tensors.
703
+ """
704
+ if isinstance(cron_schema, str):
705
+ return parse_cron_string(cron_schema)
706
+
707
+ field_keys = ["minute", "hour", "dayOfMonth", "month", "dayOfWeek"]
708
+
709
+ def kind_name(value: Any) -> str:
710
+ if isinstance(value, int):
711
+ return CronPatternType(value - 1).name if value > 0 else "WILDCARD"
712
+ return str(value).replace("CRON_FIELD_KIND_", "")
713
+
714
+ def component_from_field(field: Dict[str, Any]) -> CronComponent:
715
+ kind = kind_name(field.get("kind", "CRON_FIELD_KIND_WILDCARD"))
716
+ values = [int(v) for v in field.get("values", [])]
717
+
718
+ if kind == "WILDCARD":
719
+ return CronComponent(type=CronPatternType.WILDCARD)
720
+ if kind == "VALUE":
721
+ return CronComponent(type=CronPatternType.VALUE, values=values)
722
+ if kind == "LIST":
723
+ if field.get("items"):
724
+ items = [component_from_field(item) for item in field["items"]]
725
+ numeric_values = []
726
+ for item in items:
727
+ if item.type == CronPatternType.VALUE:
728
+ numeric_values.extend(item.values)
729
+ return CronComponent(type=CronPatternType.LIST, values=numeric_values, items=items)
730
+ return CronComponent(type=CronPatternType.LIST, values=values)
731
+ if kind == "RANGE":
732
+ return CronComponent(
733
+ type=CronPatternType.RANGE,
734
+ range_start=int(field.get("rangeStart", 0)),
735
+ range_end=int(field.get("rangeEnd", 0)),
736
+ )
737
+ if kind == "STEP":
738
+ return CronComponent(
739
+ type=CronPatternType.STEP,
740
+ step_start=int(field.get("stepStart", 0)),
741
+ step_size=int(field.get("stepSize", 1)),
742
+ )
743
+ if kind == "NTH_WEEKDAY":
744
+ return CronComponent(
745
+ type=CronPatternType.NTH_WEEKDAY,
746
+ values=values,
747
+ nth=int(field.get("nth", 1)),
748
+ )
749
+ if kind == "LAST":
750
+ return CronComponent(
751
+ type=CronPatternType.LAST,
752
+ values=values,
753
+ last_offset=int(field["lastOffset"]) if "lastOffset" in field else None,
754
+ )
755
+ if kind == "NEAREST_WEEKDAY":
756
+ return CronComponent(type=CronPatternType.NEAREST, values=values)
757
+ raise ValueError(f"Unsupported cron field kind: {kind}")
758
+
759
+ components = [component_from_field(cron_schema[key]) for key in field_keys]
760
+ semantics = cron_schema.get("domDowSemantics", "DOM_DOW_SEMANTICS_OR")
761
+ return cron_to_dict(
762
+ Cron(
763
+ minute=components[0],
764
+ hour=components[1],
765
+ dom=components[2],
766
+ month=components[3],
767
+ dow=components[4],
768
+ dom_dow_intersect=semantics == "DOM_DOW_SEMANTICS_AND",
769
+ )
770
+ )
771
+
772
+
773
+ def recursive_to_device(data, device):
774
+ """
775
+ Recursively moves data (Dict, List, or Tensor) to the specified device.
776
+ """
777
+ if isinstance(data, dict):
778
+ return {k: recursive_to_device(v, device) for k, v in data.items()}
779
+ elif isinstance(data, list):
780
+ return [recursive_to_device(v, device) for v in data]
781
+ elif isinstance(data, torch.Tensor):
782
+ return data.to(device)
783
+ elif is_dataclass(data) and not isinstance(data, type):
784
+ values = {field_name: recursive_to_device(getattr(data, field_name), device) for field_name in data.__dataclass_fields__}
785
+ return data.__class__(**values)
786
+ return data
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6c9b7a57b49c10b35fd74b21719dc593b7c262c4e4293fde3d7471853704025f
3
+ size 25368616
modeling_cronformer.py ADDED
@@ -0,0 +1,349 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Impala Systems, Inc.
2
+ # SPDX-License-Identifier: AGPL-3.0-only
3
+
4
+ from __future__ import annotations
5
+
6
+ """Canonical Cronformer model.
7
+
8
+ Cronformer is an encoder-only structured decoder: it reads a natural-language
9
+ schedule request and emits logits for the five cron fields rather than rendering
10
+ cron text token by token. The decoder uses learned field queries and learned
11
+ slot queries so each output decision has an explicit place in the model.
12
+ """
13
+
14
+ import math
15
+
16
+ import torch
17
+ from torch import nn
18
+ from transformers import AutoConfig, AutoModel, AutoModelForCausalLM, PreTrainedModel
19
+
20
+ from .configuration_cronformer import CronformerConfig
21
+ from .cron import (
22
+ COMPONENT_DIMS,
23
+ COMPONENT_NAMES,
24
+ MAX_LIST_ITEMS,
25
+ CronComponentOutput,
26
+ CronOutput,
27
+ CronPatternType,
28
+ )
29
+
30
+
31
+ SLOT_NAMES = [
32
+ "pattern",
33
+ "values",
34
+ "list_values",
35
+ "list_items",
36
+ "range_start",
37
+ "range_end",
38
+ "step_start",
39
+ "step_size",
40
+ "nth",
41
+ "last_offset",
42
+ "field_presence",
43
+ ]
44
+
45
+
46
+ class FieldSlotDecoder(nn.Module):
47
+ """Prediction heads for one cron field after field/slot attention.
48
+
49
+ The field-slot states carry global semantic context for this field, while
50
+ token-level heads preserve copy-like access to numbers and pattern words
51
+ that appear directly in the prompt.
52
+ """
53
+
54
+ def __init__(self, hidden_size: int, output_dim: int):
55
+ super().__init__()
56
+ self.output_dim = output_dim
57
+ self.token_value_norm = nn.LayerNorm(hidden_size)
58
+ self.token_step_size_norm = nn.LayerNorm(hidden_size)
59
+ self.token_pattern_norm = nn.LayerNorm(hidden_size)
60
+ self.pattern_head = nn.Linear(hidden_size, len(CronPatternType))
61
+ self.values_head = nn.Linear(hidden_size, output_dim)
62
+ self.list_values_head = nn.Linear(hidden_size, output_dim)
63
+ self.range_start_head = nn.Linear(hidden_size, output_dim)
64
+ self.range_end_head = nn.Linear(hidden_size, output_dim)
65
+ self.step_start_head = nn.Linear(hidden_size, output_dim)
66
+ self.step_size_head = nn.Linear(hidden_size, output_dim)
67
+ self.nth_head = nn.Linear(hidden_size, 6)
68
+ self.last_offset_head = nn.Linear(hidden_size, output_dim)
69
+ self.value_count_head = nn.Linear(hidden_size, output_dim + 1)
70
+ self.list_item_head = nn.Linear(hidden_size, output_dim)
71
+ self.field_presence_head = nn.Linear(hidden_size, 1)
72
+ self.token_values_head = nn.Linear(hidden_size, output_dim)
73
+ self.token_step_size_head = nn.Linear(hidden_size, output_dim)
74
+ self.token_pattern_head = nn.Linear(hidden_size, len(CronPatternType))
75
+ self.value_fusion_gate = nn.Linear(hidden_size, output_dim)
76
+ self.list_value_fusion_gate = nn.Linear(hidden_size, output_dim)
77
+ self.step_size_fusion_gate = nn.Linear(hidden_size, output_dim)
78
+
79
+ @staticmethod
80
+ def _slot(states: torch.Tensor, name: str) -> torch.Tensor:
81
+ return states[:, SLOT_NAMES.index(name), :]
82
+
83
+ @staticmethod
84
+ def _masked_token_pool(token_logits: torch.Tensor, attention_mask: torch.Tensor | None) -> torch.Tensor:
85
+ # Max pooling keeps a strong local token signal for numbers without
86
+ # forcing the structured slot state to memorize every literal value.
87
+ if attention_mask is None:
88
+ return token_logits.amax(dim=1)
89
+ mask = attention_mask.bool().unsqueeze(-1)
90
+ return token_logits.masked_fill(~mask, -1.0e4).amax(dim=1)
91
+
92
+ def forward(
93
+ self,
94
+ slot_states: torch.Tensor,
95
+ token_states: torch.Tensor,
96
+ attention_mask: torch.Tensor | None,
97
+ ) -> CronComponentOutput:
98
+ value_slot = self._slot(slot_states, "values")
99
+ list_value_slot = self._slot(slot_states, "list_values")
100
+ step_size_slot = self._slot(slot_states, "step_size")
101
+ token_values_logits = self.token_values_head(
102
+ self.token_value_norm(token_states + value_slot.unsqueeze(1))
103
+ )
104
+ token_step_size_logits = self.token_step_size_head(
105
+ self.token_step_size_norm(token_states + step_size_slot.unsqueeze(1))
106
+ )
107
+ token_pattern_logits = self.token_pattern_head(
108
+ self.token_pattern_norm(token_states + self._slot(slot_states, "pattern").unsqueeze(1))
109
+ )
110
+ pooled_token_values = self._masked_token_pool(token_values_logits, attention_mask)
111
+ pooled_token_step_size = self._masked_token_pool(token_step_size_logits, attention_mask)
112
+
113
+ # Fuse slot-level classification with token-local evidence. The gate is
114
+ # learned per output value, which lets the model decide when prompt
115
+ # literals should dominate over the semantic field representation.
116
+ value_logits = self.values_head(value_slot) + torch.sigmoid(self.value_fusion_gate(value_slot)) * pooled_token_values
117
+ list_values_logits = self.list_values_head(list_value_slot) + torch.sigmoid(
118
+ self.list_value_fusion_gate(list_value_slot)
119
+ ) * pooled_token_values
120
+ step_size_logits = self.step_size_head(step_size_slot) + torch.sigmoid(
121
+ self.step_size_fusion_gate(step_size_slot)
122
+ ) * pooled_token_step_size
123
+
124
+ list_item_state = self._slot(slot_states, "list_items")
125
+ list_item_logits = self.list_item_head(list_item_state).unsqueeze(1).expand(
126
+ -1,
127
+ MAX_LIST_ITEMS,
128
+ -1,
129
+ )
130
+ return CronComponentOutput(
131
+ pattern_logits=self.pattern_head(self._slot(slot_states, "pattern")),
132
+ values_logits=value_logits,
133
+ range_start_logits=self.range_start_head(self._slot(slot_states, "range_start")),
134
+ range_end_logits=self.range_end_head(self._slot(slot_states, "range_end")),
135
+ step_start_logits=self.step_start_head(self._slot(slot_states, "step_start")),
136
+ step_size_logits=step_size_logits,
137
+ nth_logits=self.nth_head(self._slot(slot_states, "nth")),
138
+ last_offset_logits=self.last_offset_head(self._slot(slot_states, "last_offset")),
139
+ token_values_logits=token_values_logits,
140
+ token_step_size_logits=token_step_size_logits,
141
+ token_pattern_logits=token_pattern_logits,
142
+ value_count_logits=self.value_count_head(value_slot),
143
+ list_values_logits=list_values_logits,
144
+ list_item_logits=list_item_logits,
145
+ constrained_field_logits=self.field_presence_head(self._slot(slot_states, "field_presence")),
146
+ )
147
+
148
+
149
+ class CronformerModel(PreTrainedModel):
150
+ """Field-query Cronformer.
151
+
152
+ Forward flow:
153
+ 1. Encode prompt tokens with a small Hugging Face encoder.
154
+ 2. Cross-attend five learned field queries over token states.
155
+ 3. Run field self-attention so minute/hour/dom/month/dow can coordinate.
156
+ 4. For each field, cross-attend learned slot queries over token states.
157
+ 5. Decode slot states into the CronOutput contract used by training/eval.
158
+ """
159
+
160
+ config_class = CronformerConfig
161
+
162
+ def __init__(self, config: CronformerConfig):
163
+ super().__init__(config)
164
+ self.config = config
165
+ self.encoder = AutoModel.from_config(config.to_encoder_config())
166
+ encoder_dim = self.encoder.config.hidden_size
167
+ hidden_size = config.hidden_size
168
+ self.token_projection = nn.Sequential(
169
+ nn.Linear(encoder_dim, hidden_size),
170
+ nn.GELU(),
171
+ nn.LayerNorm(hidden_size),
172
+ )
173
+ self.field_queries = nn.Parameter(torch.empty(len(COMPONENT_NAMES), hidden_size))
174
+ self.slot_queries = nn.Parameter(torch.empty(len(SLOT_NAMES), hidden_size))
175
+ self.field_attention = nn.MultiheadAttention(
176
+ hidden_size,
177
+ config.num_attention_heads,
178
+ dropout=config.dropout,
179
+ batch_first=True,
180
+ )
181
+ field_layer = nn.TransformerEncoderLayer(
182
+ d_model=hidden_size,
183
+ nhead=config.num_attention_heads,
184
+ dim_feedforward=hidden_size * 4,
185
+ dropout=config.dropout,
186
+ activation="gelu",
187
+ batch_first=True,
188
+ norm_first=True,
189
+ )
190
+ self.field_interaction = nn.TransformerEncoder(
191
+ field_layer,
192
+ num_layers=config.num_field_layers,
193
+ )
194
+ self.slot_attention = nn.MultiheadAttention(
195
+ hidden_size,
196
+ config.num_attention_heads,
197
+ dropout=config.dropout,
198
+ batch_first=True,
199
+ )
200
+ self.slot_norm = nn.LayerNorm(hidden_size)
201
+ self.heads = nn.ModuleDict(
202
+ {
203
+ name: FieldSlotDecoder(hidden_size, output_dim)
204
+ for name, output_dim in zip(COMPONENT_NAMES, COMPONENT_DIMS)
205
+ }
206
+ )
207
+ self.dom_dow_pool = nn.Sequential(
208
+ nn.Linear(hidden_size, hidden_size),
209
+ nn.GELU(),
210
+ nn.LayerNorm(hidden_size),
211
+ )
212
+ self.dom_dow_intersect_head = nn.Linear(hidden_size, 2)
213
+ self.post_init()
214
+ nn.init.normal_(self.field_queries, mean=0.0, std=0.02)
215
+ nn.init.normal_(self.slot_queries, mean=0.0, std=0.02)
216
+
217
+ def _init_weights(self, module: nn.Module):
218
+ if isinstance(module, nn.Linear):
219
+ module.weight.data.normal_(mean=0.0, std=0.02)
220
+ if module.bias is not None:
221
+ module.bias.data.zero_()
222
+ elif isinstance(module, nn.LayerNorm):
223
+ module.bias.data.zero_()
224
+ module.weight.data.fill_(1.0)
225
+
226
+ def _key_padding_mask(self, attention_mask: torch.Tensor | None) -> torch.Tensor | None:
227
+ if attention_mask is None:
228
+ return None
229
+ return ~attention_mask.bool()
230
+
231
+ def _token_states(
232
+ self,
233
+ input_ids: torch.LongTensor,
234
+ attention_mask: torch.Tensor | None,
235
+ ) -> torch.Tensor:
236
+ encoder_output = self.encoder(input_ids=input_ids, attention_mask=attention_mask)
237
+ return self.token_projection(encoder_output.last_hidden_state)
238
+
239
+ def _field_states(
240
+ self,
241
+ token_states: torch.Tensor,
242
+ attention_mask: torch.Tensor | None,
243
+ ) -> torch.Tensor:
244
+ batch_size = token_states.shape[0]
245
+ queries = self.field_queries.unsqueeze(0).expand(batch_size, -1, -1)
246
+ # Each learned field query asks the token sequence for the evidence
247
+ # relevant to one cron field. This is the Direct+V2 merge point: output
248
+ # remains structured, but field semantics are learned by attention.
249
+ field_states, _ = self.field_attention(
250
+ queries,
251
+ token_states,
252
+ token_states,
253
+ key_padding_mask=self._key_padding_mask(attention_mask),
254
+ need_weights=False,
255
+ )
256
+ # Field interaction is the only learned global coordination layer. It
257
+ # replaces hand-coded semantic corrections while still allowing fields
258
+ # such as day-of-month and day-of-week to influence each other.
259
+ return self.field_interaction(field_states)
260
+
261
+ def _slot_states(
262
+ self,
263
+ field_states: torch.Tensor,
264
+ token_states: torch.Tensor,
265
+ attention_mask: torch.Tensor | None,
266
+ ) -> dict[str, torch.Tensor]:
267
+ slot_states = {}
268
+ slot_offsets = self.slot_queries.unsqueeze(0)
269
+ key_padding_mask = self._key_padding_mask(attention_mask)
270
+ for index, name in enumerate(COMPONENT_NAMES):
271
+ # Slot queries specialize a field state into concrete decisions:
272
+ # pattern, values, ranges, steps, list items, and field presence.
273
+ queries = field_states[:, index : index + 1, :] + slot_offsets
274
+ attended, _ = self.slot_attention(
275
+ queries,
276
+ token_states,
277
+ token_states,
278
+ key_padding_mask=key_padding_mask,
279
+ need_weights=False,
280
+ )
281
+ slot_states[name] = self.slot_norm(attended + queries)
282
+ return slot_states
283
+
284
+ def forward(
285
+ self,
286
+ input_ids: torch.LongTensor,
287
+ attention_mask: torch.Tensor | None = None,
288
+ token_type_ids: torch.Tensor | None = None,
289
+ **_: object,
290
+ ) -> CronOutput:
291
+ token_states = self._token_states(input_ids, attention_mask)
292
+ field_states = self._field_states(token_states, attention_mask)
293
+ slot_states = self._slot_states(field_states, token_states, attention_mask)
294
+ component_outputs = {}
295
+ count_decode_components = set(getattr(self.config, "value_count_decoding_components", [0, 1]))
296
+ token_decode_components = set(getattr(self.config, "token_value_decoding_components", [0, 1]))
297
+ list_item_components = getattr(self.config, "list_item_components", None)
298
+ list_item_component_set = None if list_item_components is None else set(list_item_components)
299
+ for component_index, name in enumerate(COMPONENT_NAMES):
300
+ output = self.heads[name](slot_states[name], token_states, attention_mask)
301
+ # These flags are decoder policy switches consumed by cron.py. The
302
+ # model always emits the full CronComponentOutput; configs choose
303
+ # which learned logits participate in final cron reconstruction.
304
+ output.use_value_count_decoding = (
305
+ self.config.use_value_count_decoding and component_index in count_decode_components
306
+ )
307
+ output.use_token_value_decoding = (
308
+ self.config.use_token_value_decoding and component_index in token_decode_components
309
+ )
310
+ output.use_path_score_decoding = self.config.use_path_score_decoding
311
+ output.use_list_item_decoding = self.config.use_list_item_decoding and (
312
+ list_item_component_set is None or component_index in list_item_component_set
313
+ )
314
+ output.use_list_item_scoring = self.config.use_list_item_scoring and (
315
+ list_item_component_set is None or component_index in list_item_component_set
316
+ )
317
+ output.list_item_score_scale = self.config.list_item_score_scale
318
+ component_outputs[name] = output
319
+
320
+ pooled_fields = field_states.mean(dim=1)
321
+ dom_dow_logits = self.dom_dow_intersect_head(self.dom_dow_pool(pooled_fields))
322
+ return CronOutput(
323
+ minute=component_outputs["minute"],
324
+ hour=component_outputs["hour"],
325
+ dom=component_outputs["dom"],
326
+ month=component_outputs["month"],
327
+ dow=component_outputs["dow"],
328
+ dom_dow_intersect_logits=dom_dow_logits,
329
+ )
330
+
331
+ @classmethod
332
+ def from_encoder(cls, encoder_model: str = "google/bert_uncased_L-2_H-128_A-2") -> "CronformerModel":
333
+ config = CronformerConfig(encoder_model=encoder_model)
334
+ model = cls(config)
335
+ encoder_config = AutoConfig.from_pretrained(encoder_model)
336
+ architectures = getattr(encoder_config, "architectures", None) or []
337
+ # Some small language models expose their reusable token backbone behind
338
+ # AutoModelForCausalLM rather than AutoModel. Keep this loader narrow:
339
+ # Cronformer still uses hidden states as an encoder, never generation.
340
+ if any(str(architecture).endswith("ForCausalLM") for architecture in architectures):
341
+ causal_lm = AutoModelForCausalLM.from_pretrained(encoder_model)
342
+ if not hasattr(causal_lm, "model"):
343
+ raise ValueError(
344
+ f"{encoder_model} advertises a CausalLM architecture but does not expose a .model backbone"
345
+ )
346
+ model.encoder = causal_lm.model
347
+ else:
348
+ model.encoder = AutoModel.from_pretrained(encoder_model)
349
+ return model
special_tokens_map.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "cls_token": "[CLS]",
3
+ "mask_token": "[MASK]",
4
+ "pad_token": "[PAD]",
5
+ "sep_token": "[SEP]",
6
+ "unk_token": "[UNK]"
7
+ }
tokenizer_config.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "clean_up_tokenization_spaces": true,
3
+ "cls_token": "[CLS]",
4
+ "do_basic_tokenize": true,
5
+ "do_lower_case": true,
6
+ "mask_token": "[MASK]",
7
+ "model_max_length": 128,
8
+ "never_split": null,
9
+ "pad_token": "[PAD]",
10
+ "sep_token": "[SEP]",
11
+ "strip_accents": null,
12
+ "tokenize_chinese_chars": true,
13
+ "tokenizer_class": "BertTokenizerFast",
14
+ "unk_token": "[UNK]"
15
+ }
vocab.txt ADDED
The diff for this file is too large to render. See raw diff