Compare commits

...

10 Commits

Author SHA256 Message Date
0a879cbfe9 removes debug line, writes json to file instead (path: output/) 2026-02-13 11:49:59 +01:00
f60b58f2f2 ignores output of main.py (output/*.json) 2026-02-13 11:49:13 +01:00
6f618b2340 adds comments 2026-02-13 01:05:32 +01:00
38940995b5 completes the dataset with instruments_used (in a way...)
only lacks units of measurement, then I'll be ready for conversion
2026-02-13 00:59:22 +01:00
f686ea65b1 adds get_instruments method to Layer class
get_instruments returns a dictionary with the names of every system used
during the deposition
unfortunately, NeXus standard allows for a single value of all three
keys per every sample - not every layer
this means that every layer has its own data for laser, rheed system and
depo chamber which IDEALLY is the same for every layer, but in practice
they COULD be different and I still don't know how to deal with this
2026-02-13 00:32:31 +01:00
23bfdefd30 adds all the remaining layer data
only lacks the instrument_used data and units of measurement
NOTE: units of measurement are hard to collect, but could be assumed
considering our instruments are standard
2026-02-13 00:18:07 +01:00
38d281543e code cleanup: deletes debug lines from main.py 2026-02-13 00:07:39 +01:00
a12506b8be MAJOR: main.py successfully produces JSON following NeXus-schema
takes API key and elabid of the "entrypoint" sample as input
returns indented JSON with the reconstructed dataset!
currently lacks instruments_used data (matter of minutes)
and all the layer data (already present in Layer-class objects)
2026-02-13 00:01:24 +01:00
43cfd788f3 adds non-req. attr. "description" to class Target 2026-02-12 23:53:21 +01:00
da42de5466 handles error 400 bad request with exit message 2026-02-12 23:52:37 +01:00
5 changed files with 127 additions and 62 deletions

5
.gitignore vendored
View File

@@ -1,6 +1,9 @@
# ignora log di h5tojson e jsontoh5 # ignores logs of h5tojson, jsontoh5
*.log *.log
# ignores output json of main.py
output/*.json
# ---> Python # ---> Python
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/

0
output/placeholder Normal file
View File

View File

@@ -33,6 +33,8 @@ class APIHandler:
raise ConnectionError(f"Invalid API key or authentication method.") raise ConnectionError(f"Invalid API key or authentication method.")
case 404: case 404:
raise ConnectionError(f"404: Not Found. This means there's no resource with this elabid (wrong elabid?) on your eLabFTW (wrong endpoint?).") raise ConnectionError(f"404: Not Found. This means there's no resource with this elabid (wrong elabid?) on your eLabFTW (wrong endpoint?).")
case 400:
raise ConnectionError(f"400: Bad Request. This means the API endpoint you tried to reach is invalid. Did you tamper with the source code? If not, contact the developer.")
case _: case _:
raise ConnectionError(f"HTTP request failed with status code: {response.status_code} (NOTE: 4xx means user's fault).") raise ConnectionError(f"HTTP request failed with status code: {response.status_code} (NOTE: 4xx means user's fault).")
else: else:

View File

@@ -16,8 +16,9 @@ class Layer:
self.extra = layer_data["metadata_decoded"]["extra_fields"] self.extra = layer_data["metadata_decoded"]["extra_fields"]
self.layer_number = self.extra["Layer Progressive Number"]["value"] # integer self.layer_number = self.extra["Layer Progressive Number"]["value"] # integer
self.target_elabid = self.extra["Target"]["value"] # elabid self.target_elabid = self.extra["Target"]["value"] # elabid
self.rheed_system_elabid = self.extra["RHEED System"]["value"] # elabid
self.laser_system_elabid = self.extra["Laser System"]["value"] # elabid self.laser_system_elabid = self.extra["Laser System"]["value"] # elabid
self.chamber_elabid = self.extra["Chamber"]["value"] # elabid
self.rheed_system_elabid = self.extra["RHEED System"]["value"] # elabid
self.start_time = layer_data.get("created_at") self.start_time = layer_data.get("created_at")
self.operator = layer_data.get("fullname") self.operator = layer_data.get("fullname")
self.description = layer_data.get("body") self.description = layer_data.get("body")
@@ -71,6 +72,16 @@ class Layer:
# Some keys are not required and can be called through the .get() method - which is permissive and allows null values; # Some keys are not required and can be called through the .get() method - which is permissive and allows null values;
# Other keys are required so if they can't be called (invalid or null) raise error and stop execution of the program: # Other keys are required so if they can't be called (invalid or null) raise error and stop execution of the program:
raise KeyError(f"The provided dictionary lacks a \"{k}\" key. Check the deposition layer entry on eLabFTW and make sure you used the correct Experiment template.") raise KeyError(f"The provided dictionary lacks a \"{k}\" key. Check the deposition layer entry on eLabFTW and make sure you used the correct Experiment template.")
def get_instruments(self, apikey):
raw_lasersys_data = APIHandler(apikey).get_entry_from_elabid(self.laser_system_elabid, entryType="items")
raw_chamber_data = APIHandler(apikey).get_entry_from_elabid(self.chamber_elabid, entryType="items")
raw_rheedsys_data = APIHandler(apikey).get_entry_from_elabid(self.rheed_system_elabid, entryType="items")
instruments_used = {
"laser_system": raw_lasersys_data.get("title") or None,
"deposition_chamber": raw_chamber_data.get("title") or None,
"rheed_system": raw_rheedsys_data.get("title") or None,
}
return instruments_used
class Entrypoint: class Entrypoint:
''' '''
@@ -157,6 +168,8 @@ class Target(Material):
self.manufacturer = self.extra["Supplier"]["value"] self.manufacturer = self.extra["Supplier"]["value"]
except KeyError as k: except KeyError as k:
raise KeyError(f"The provided dictionary lacks a \"{k}\" key - which is specific for PLD targets. Check the {self.name} target entry on eLabFTW and make sure you used the correct Resource template.") raise KeyError(f"The provided dictionary lacks a \"{k}\" key - which is specific for PLD targets. Check the {self.name} target entry on eLabFTW and make sure you used the correct Resource template.")
# Non-required attributes:
self.description = material_data.get("body") or ""

View File

@@ -98,71 +98,118 @@ def chain_layer_to_target(layer_object):
material_object = call_material_from_elabid(target_elabid) material_object = call_material_from_elabid(target_elabid)
return material_object return material_object
#sample_object = call_entrypoint_from_elabid(elabid) def deduplicate_instruments_from_layers(layers):
#from_entrypoint_to_material(sample_object) '''
Takes a list of Layer-class objects and for each layer gets the instruments used (laser, depo chamber and RHEED), returns deduplicated list. Ideally, the lists should only contain one element.
'''
lasers = []
chambers = []
rheeds = []
for lyr in layers:
instruments = lyr.get_instruments(apikey)
lasers.append(instruments["laser_system"])
chambers.append(instruments["deposition_chamber"])
rheeds.append(instruments["rheed_system"])
instruments_used_dict = {
"laser_system": list( set( lasers ) ),
"deposition_chamber": list( set( chambers ) ),
"rheed_system" : list( set( rheeds ) ),
}
# lasers = { f"layer_{lyr.layer_number}": lyr.laser_system for lyr in layers }
# chambers = { f"layer_{lyr.layer_number}": lyr.deposition_chamber for lyr in layers }
# rheeds = { f"layer_{lyr.layer_number}": lyr.rheed_system for lyr in layers }
# instruments_used_dict = {
# "laser_system": lasers,
# "deposition_chamber": chambers,
# "rheed_system": rheeds,
# }
return instruments_used_dict
def make_nexus_schema_dictionary(substrate_object, layers):
'''
Main function, takes all the other functions to reconstruct the full dataset. Takes a Substrate-class object (output of the chain_entrypoint_to_batch() function) and a list of Layer-class objects (output of the chain_entrypoint_to_layers() function), returns dictionary with the same schema as the NeXus standard for PLD fabrications.
'''
pld_fabrication = {
"sample": {
"substrate": {
"name": substrate_object.name,
"chemical_formula" : substrate_object.get_compound_formula(apikey),
"orientation" : substrate_object.orientation,
"miscut_angle" : substrate_object.miscut_angle,
"miscut_direction" : substrate_object.miscut_direction,
"thickness" : substrate_object.thickness,
"dimensions" : substrate_object.dimensions,
"surface_treatment" : substrate_object.surface_treatment,
"manufacturer" : substrate_object.manufacturer,
"batch_id" : substrate_object.batch_id,
},
"multilayer": {},
},
"instruments_used": deduplicate_instruments_from_layers(layers),
}
multilayer = pld_fabrication["sample"]["multilayer"]
for layer in layers:
name = "layer_" + layer.layer_number
target_object = chain_layer_to_target(layer)
target_dict = {
"name": target_object.name,
"chemical_formula" : target_object.get_compound_formula(apikey),
"description" : target_object.description,
"shape" : target_object.shape,
"dimensions" : target_object.dimensions,
"thickness" : target_object.thickness,
"solid_form" : target_object.solid_form,
"manufacturer" : target_object.manufacturer,
# TO-DO: currently not available:
# "batch_id" : target_object.batch_id,
}
multilayer[name] = {
"target": target_dict,
"start_time": layer.start_time,
"operator": layer.operator,
"description": layer.description,
"number_of_pulses": layer.number_of_pulses,
"deposition_time": layer.deposition_time,
"temperature": layer.temperature,
"heating_method": layer.heating_method,
"layer_thickness": layer.layer_thickness,
"buffer_gas": layer.buffer_gas,
"process_pressure": layer.process_pressure,
"heater_target_distance": layer.heater_target_distance,
"repetition_rate": layer.repetition_rate,
"laser_fluence": layer.laser_fluence,
"laser_spot_area": layer.laser_spot_area,
"laser_energy": layer.laser_energy,
"laser_rastering": {
"geometry": layer.laser_rastering_geometry,
"positions": layer.laser_rastering_positions,
"velocities": layer.laser_rastering_velocities,
},
"pre_annealing": {
"ambient_gas": layer.pre_annealing_ambient_gas,
"pressure": layer.pre_annealing_pressure,
"temperature": layer.pre_annealing_temperature,
"duration": layer.pre_annealing_duration,
},
"post_annealing": {
"ambient_gas": layer.post_annealing_ambient_gas,
"pressure": layer.post_annealing_pressure,
"temperature": layer.post_annealing_temperature,
"duration": layer.post_annealing_duration,
},
}
return pld_fabrication
if __name__=="__main__": if __name__=="__main__":
print(f"=======================\n===== DEBUG MODE! =====\n=======================\n") # TO-DO: place the API base URL somewhere else.
ELABFTW_API_URL = "https://elabftw.fisica.unina.it/api/v2" ELABFTW_API_URL = "https://elabftw.fisica.unina.it/api/v2"
apikey = getpass("Paste API key here: ") apikey = getpass("Paste API key here: ")
elabid = input("Enter elabid of your starting sample [default= 1111]: ") or 1111 elabid = input("Enter elabid of your starting sample [default= 1111]: ") or 1111
data = APIHandler(apikey).get_entry_from_elabid(elabid) data = APIHandler(apikey).get_entry_from_elabid(elabid)
sample = Entrypoint(data) sample = Entrypoint(data)
batch = chain_entrypoint_to_batch(sample) # Material-class object substrate_object = chain_entrypoint_to_batch(sample) # Substrate-class object
bd = batch.__dict__
bd.pop("extra")
layers = chain_entrypoint_to_layers(sample) # list of Layer-class objects layers = chain_entrypoint_to_layers(sample) # list of Layer-class objects
print(f"Sample name:\n{sample.name}\n") result = make_nexus_schema_dictionary(substrate_object, layers)
print(f"Substrate data:\n{bd}\n") # print(make_nexus_schema_dictionary(substrate_object, layers)) # debug
print(f"Layers data:") with open (f"output/sample-{elabid}.json", "w") as f:
for layer in layers: json.dump(result, f, indent=3)
ld = layer.__dict__
ld.pop("extra")
tgt = chain_layer_to_target(layer)
td = tgt.__dict__
td.pop("extra")
print(ld)
print(td)
print()
# entryType = None
# while entryType not in ["items", "experiments"]:
# eT = input("Enter a valid entry type [items, experiments]: ")
# # This allows for a shortcut: instead of prompting the type before and the elabid after I can just prompt both at the same time - e.g. e51 is exp. 51, i1108 is item 1108...
# if eT[0] in ["e", "i"] and eT[-1].isnumeric():
# try:
# elabid = int(eT[1:])
# eT = eT[0]
# except Exception:
# print("Usage: i|item|items|i[ELABID] for items, e|experiment|experiments|e[ELABID] for experiments.")
# continue
# match eT:
# case "items" | "i" | "item":
# entryType = "items"
# case "experiments" | "e" | "exp" | "experiment":
# entryType = "experiments"
# case _:
# continue
# # This will probably be reworked in production
# try:
# elabid = elabid
# except NameError:
# elabid = input("Input elabid here [default = 1111]: ") or 1111
# data = APIHandler(apikey).get_entry_from_elabid(elabid, entryType)
# if entryType == "experiments":
# layer = Layer(data)
# result = layer.__dict__
# result.pop("extra")
# print(result)
# elif entryType == "items":
# if data.get("category_title") == "Sample":
# item = Entrypoint(data)
# elif data.get("category_title") in ["PLD Target", "Substrate"]:
# item = Material(data)
# print(item.get_compound_formula(apikey))
# else:
# raise Exception("The selected item or experiment is not in one of the following categories: [Sample, PLD Target, Substrate, PLD Deposition].")
# result = item.__dict__
# result.pop("extra")
# print(result)