|
18 | 18 | # When imported dynamically, use absolute import |
19 | 19 | import sys |
20 | 20 | from pathlib import Path |
| 21 | + |
21 | 22 | sys.path.insert(0, str(Path(__file__).parent)) |
22 | 23 | from base import ExcludeFromUpload |
23 | 24 |
|
24 | 25 |
|
25 | 26 | class Experiment(BaseModel, extra="forbid"): |
26 | 27 | """ |
27 | 28 | Main experiment schema with all 1:1 data consolidated. |
28 | | - |
| 29 | +
|
29 | 30 | This is the primary table - one row per experiment. |
30 | 31 | All related 1:1 data (heating, recovery, xrd, finalization, dosing) is merged here. |
31 | 32 | """ |
32 | | - |
| 33 | + |
33 | 34 | # === Core experiment fields === |
34 | | - experiment_id: str = Field( |
35 | | - description="Unique experiment identifier (MongoDB _id)" |
36 | | - ) |
37 | | - |
38 | | - name: str = Field( |
39 | | - description="Experiment name (e.g., NSC_249, MINES_12)" |
40 | | - ) |
41 | | - |
| 35 | + experiment_id: str = Field(description="Unique experiment identifier (MongoDB _id)") |
| 36 | + |
| 37 | + name: str = Field(description="Experiment name (e.g., NSC_249, MINES_12)") |
| 38 | + |
42 | 39 | experiment_type: str = Field( |
43 | 40 | description="Root experiment type (NSC, Na, PG, MINES, TRI)" |
44 | 41 | ) |
45 | | - |
| 42 | + |
46 | 43 | experiment_subgroup: str | None = Field( |
47 | | - default=None, |
48 | | - description="Experiment subgroup (e.g., NSC_249, Na_123)" |
| 44 | + default=None, description="Experiment subgroup (e.g., NSC_249, Na_123)" |
49 | 45 | ) |
50 | | - |
51 | | - target_formula: str = Field( |
52 | | - description="Target chemical formula" |
53 | | - ) |
54 | | - |
55 | | - last_updated: datetime = Field( |
56 | | - description="Last modification timestamp" |
57 | | - ) |
58 | | - |
| 46 | + |
| 47 | + target_formula: str = Field(description="Target chemical formula") |
| 48 | + |
| 49 | + last_updated: datetime = Field(description="Last modification timestamp") |
| 50 | + |
59 | 51 | status: Literal["completed", "error", "active", "unknown"] = Field( |
60 | 52 | description="Workflow status" |
61 | 53 | ) |
62 | | - |
| 54 | + |
63 | 55 | notes: str | None = Field( |
64 | | - default=None, |
65 | | - description="Optional notes about the experiment" |
| 56 | + default=None, description="Optional notes about the experiment" |
66 | 57 | ) |
67 | 58 |
|
68 | 59 | # === Heating fields (prefix: heating_) === |
69 | 60 | heating_method: Literal["standard", "atmosphere", "manual", "none"] | None = Field( |
70 | | - default=None, |
71 | | - description="Heating method used" |
| 61 | + default=None, description="Heating method used" |
72 | 62 | ) |
73 | | - |
| 63 | + |
74 | 64 | heating_temperature: float | None = Field( |
75 | | - default=None, |
76 | | - description="Target heating temperature in °C" |
| 65 | + default=None, description="Target heating temperature in °C" |
77 | 66 | ) |
78 | | - |
| 67 | + |
79 | 68 | heating_time: float | None = Field( |
80 | | - default=None, |
81 | | - description="Heating duration in minutes" |
| 69 | + default=None, description="Heating duration in minutes" |
82 | 70 | ) |
83 | | - |
| 71 | + |
84 | 72 | heating_cooling_rate: float | None = Field( |
85 | | - default=None, |
86 | | - description="Cooling rate in °C/min" |
| 73 | + default=None, description="Cooling rate in °C/min" |
87 | 74 | ) |
88 | | - |
| 75 | + |
89 | 76 | heating_atmosphere: str | None = Field( |
90 | | - default=None, |
91 | | - description="Atmosphere used during heating (e.g., N2, Ar, Air)" |
| 77 | + default=None, description="Atmosphere used during heating (e.g., N2, Ar, Air)" |
92 | 78 | ) |
93 | | - |
| 79 | + |
94 | 80 | heating_flow_rate_ml_min: float | None = Field( |
95 | | - default=None, |
96 | | - description="Gas flow rate during heating in mL/min" |
| 81 | + default=None, description="Gas flow rate during heating in mL/min" |
97 | 82 | ) |
98 | | - |
| 83 | + |
99 | 84 | heating_low_temp_calcination: bool | None = Field( |
100 | | - default=None, |
101 | | - description="Whether low temperature calcination was used" |
| 85 | + default=None, description="Whether low temperature calcination was used" |
102 | 86 | ) |
103 | 87 |
|
104 | 88 | # === Recovery fields (prefix: recovery_) === |
105 | 89 | recovery_total_dosed_mass_mg: float | None = Field( |
106 | | - default=None, |
107 | | - description="Total mass of all powders dosed in mg" |
| 90 | + default=None, description="Total mass of all powders dosed in mg" |
108 | 91 | ) |
109 | | - |
| 92 | + |
110 | 93 | # EXCLUDED FROM UPLOAD per team request |
111 | 94 | recovery_weight_collected_mg: float | None = ExcludeFromUpload( |
112 | 95 | description="Weight of powder collected after heating in mg (EMBARGOED)" |
113 | 96 | ) |
114 | | - |
| 97 | + |
115 | 98 | recovery_yield_percent: float | None = Field( |
116 | | - default=None, |
117 | | - description="Recovery yield (collected / dosed * 100)" |
| 99 | + default=None, description="Recovery yield (collected / dosed * 100)" |
118 | 100 | ) |
119 | | - |
| 101 | + |
120 | 102 | recovery_initial_crucible_weight_mg: float | None = Field( |
121 | | - default=None, |
122 | | - description="Initial crucible weight before experiment in mg" |
| 103 | + default=None, description="Initial crucible weight before experiment in mg" |
123 | 104 | ) |
124 | | - |
| 105 | + |
125 | 106 | recovery_failure_classification: str | None = Field( |
126 | | - default=None, |
127 | | - description="Classification of any failure during recovery" |
| 107 | + default=None, description="Classification of any failure during recovery" |
128 | 108 | ) |
129 | 109 |
|
130 | 110 | # === XRD measurement fields (prefix: xrd_) === |
131 | 111 | xrd_sampleid_in_aeris: str | None = Field( |
132 | | - default=None, |
133 | | - description="Sample ID in Aeris XRD system" |
| 112 | + default=None, description="Sample ID in Aeris XRD system" |
134 | 113 | ) |
135 | | - |
| 114 | + |
136 | 115 | xrd_holder_index: int | None = Field( |
137 | | - default=None, |
138 | | - description="XRD sample holder position index" |
| 116 | + default=None, description="XRD sample holder position index" |
139 | 117 | ) |
140 | | - |
| 118 | + |
141 | 119 | # EXCLUDED FROM UPLOAD per team request |
142 | 120 | xrd_total_mass_dispensed_mg: float | None = ExcludeFromUpload( |
143 | 121 | description="Mass dispensed for XRD measurement in mg (EMBARGOED)" |
144 | 122 | ) |
145 | | - |
| 123 | + |
146 | 124 | xrd_met_target_mass: bool | None = Field( |
147 | | - default=None, |
148 | | - description="Whether target mass was achieved for XRD" |
| 125 | + default=None, description="Whether target mass was achieved for XRD" |
149 | 126 | ) |
150 | 127 |
|
151 | 128 | # === Finalization fields (prefix: finalization_) === |
152 | 129 | finalization_decoded_sample_id: str | None = Field( |
153 | | - default=None, |
154 | | - description="Decoded sample ID from barcode" |
| 130 | + default=None, description="Decoded sample ID from barcode" |
155 | 131 | ) |
156 | | - |
| 132 | + |
157 | 133 | finalization_successful_labeling: bool | None = Field( |
158 | | - default=None, |
159 | | - description="Whether sample was successfully labeled" |
| 134 | + default=None, description="Whether sample was successfully labeled" |
160 | 135 | ) |
161 | | - |
| 136 | + |
162 | 137 | finalization_storage_location: str | None = Field( |
163 | | - default=None, |
164 | | - description="Final storage location of sample" |
| 138 | + default=None, description="Final storage location of sample" |
165 | 139 | ) |
166 | 140 |
|
167 | 141 | # === Dosing fields (prefix: dosing_) === |
168 | 142 | dosing_crucible_position: int | None = Field( |
169 | | - default=None, |
170 | | - description="Crucible position in rack", |
171 | | - ge=1, |
172 | | - le=4 |
173 | | - ) |
174 | | - |
175 | | - dosing_crucible_sub_rack: Literal["SubRackA", "SubRackB", "SubRackC", "SubRackD"] | None = Field( |
176 | | - default=None, |
177 | | - description="Sub-rack identifier" |
| 143 | + default=None, description="Crucible position in rack", ge=1, le=4 |
178 | 144 | ) |
179 | | - |
| 145 | + |
| 146 | + dosing_crucible_sub_rack: ( |
| 147 | + Literal["SubRackA", "SubRackB", "SubRackC", "SubRackD"] | None |
| 148 | + ) = Field(default=None, description="Sub-rack identifier") |
| 149 | + |
180 | 150 | dosing_mixing_pot_position: int | None = Field( |
181 | | - default=None, |
182 | | - description="Mixing pot position", |
183 | | - ge=1, |
184 | | - le=16 |
| 151 | + default=None, description="Mixing pot position", ge=1, le=16 |
185 | 152 | ) |
186 | | - |
| 153 | + |
187 | 154 | dosing_ethanol_dispense_volume: int | None = Field( |
188 | | - default=None, |
189 | | - description="Volume of ethanol dispensed in µL", |
190 | | - ge=0 |
| 155 | + default=None, description="Volume of ethanol dispensed in µL", ge=0 |
191 | 156 | ) |
192 | | - |
| 157 | + |
193 | 158 | dosing_target_transfer_volume: int | None = Field( |
194 | | - default=None, |
195 | | - description="Target transfer volume in µL", |
196 | | - ge=0 |
| 159 | + default=None, description="Target transfer volume in µL", ge=0 |
197 | 160 | ) |
198 | | - |
| 161 | + |
199 | 162 | dosing_actual_transfer_mass: float | None = Field( |
200 | | - default=None, |
201 | | - description="Actual mass transferred in g", |
202 | | - ge=0 |
| 163 | + default=None, description="Actual mass transferred in g", ge=0 |
203 | 164 | ) |
204 | | - |
| 165 | + |
205 | 166 | dosing_dac_duration: int | None = Field( |
206 | | - default=None, |
207 | | - description="DAC duration in seconds", |
208 | | - ge=0 |
| 167 | + default=None, description="DAC duration in seconds", ge=0 |
209 | 168 | ) |
210 | | - |
| 169 | + |
211 | 170 | dosing_dac_speed: int | None = Field( |
212 | | - default=None, |
213 | | - description="DAC rotation speed in rpm", |
214 | | - ge=0 |
| 171 | + default=None, description="DAC rotation speed in rpm", ge=0 |
215 | 172 | ) |
216 | | - |
| 173 | + |
217 | 174 | dosing_actual_heat_duration: int | None = Field( |
218 | 175 | default=None, |
219 | 176 | description="Actual heating duration during dosing in seconds", |
220 | | - ge=0 |
| 177 | + ge=0, |
221 | 178 | ) |
222 | | - |
| 179 | + |
223 | 180 | dosing_end_reason: str | None = Field( |
224 | | - default=None, |
225 | | - description="Reason for ending dosing session" |
| 181 | + default=None, description="Reason for ending dosing session" |
226 | 182 | ) |
227 | | - |
0 commit comments