Skip to content
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ ipython_config.py
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
.conda
bootstrap_requirements.txt
environment.yml

# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
Expand Down
314 changes: 126 additions & 188 deletions docs/examples/Pulse_Building_Tutorial.ipynb
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are there two duplicated(?) plots instead of one in this example? or is it github mis-rendering?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be working fine in VScode.

Large diffs are not rendered by default.

136 changes: 131 additions & 5 deletions src/broadbean/blueprint.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# This file is for defining the blueprint object

import functools as ft
import inspect
import json
import re
import warnings
Expand Down Expand Up @@ -81,9 +82,15 @@ def __init__(
# Make special functions live in the funlist but transfer their names
# to the namelist
# Infer names from signature if not given, i.e. allow for '' names
# for ii, name in enumerate(namelist):
# if isinstance(funlist[ii], str):
# namelist[ii] = funlist[ii]
# elif name == "":
# namelist[ii] = funlist[ii].__name__
for ii, name in enumerate(namelist):
if isinstance(funlist[ii], str):
namelist[ii] = funlist[ii]
if name == "":
namelist[ii] = funlist[ii]
elif name == "":
namelist[ii] = funlist[ii].__name__
Comment thread
bennthomsen marked this conversation as resolved.
Outdated

Expand All @@ -93,8 +100,8 @@ def __init__(
argslist[ii] = (args,)
self._argslist = argslist

self._namelist = namelist
namelist = self._make_names_unique(namelist)
self._namelist = namelist
Comment thread
bennthomsen marked this conversation as resolved.

# initialise markers
if marker1 is None:
Expand Down Expand Up @@ -265,6 +272,55 @@ def description(self):
desc[segkey]["durations"] = self._durslist[sn]
if desc[segkey]["function"] == "waituntil":
desc[segkey]["arguments"] = {"waittime": self._argslist[sn]}
elif desc[segkey]["function"] == "function PulseAtoms.arb_func":
# Special handling for arb_func serialization
func_obj, kwargs_dict = self._argslist[sn]

# Serialize the function
if hasattr(func_obj, "__name__") and func_obj.__name__ != "<lambda>":
Comment thread
bennthomsen marked this conversation as resolved.
Outdated
# Regular function - store name and try to get source
func_name = func_obj.__name__
try:
func_source = inspect.getsource(func_obj)
Comment thread
bennthomsen marked this conversation as resolved.
Outdated
except (OSError, TypeError):
func_source = None
desc[segkey]["arguments"] = {
"func_type": "named_function",
"func_name": func_name,
"func_source": func_source,
"kwargs": kwargs_dict,
}
else:
# Lambda function - store source code
# First check if the lambda has a __func_source__ attribute
# (for dynamically created lambdas)
if hasattr(func_obj, "__func_source__"):
func_source = func_obj.__func_source__
else:
# Fall back to inspect.getsource() with regex parsing
try:
func_source = inspect.getsource(func_obj)
# Extract just the lambda part using regex
import re

# Match 'lambda' followed by parameters, colon, and expression
# This handles nested parentheses and complex expressions
lambda_match = re.search(
r"lambda\s+[^:]*:\s*[^\n,;]+", func_source
)
if lambda_match:
func_source = lambda_match.group(0).strip()
else:
func_source = "lambda t, **kwargs: 0"
except (OSError, TypeError):
# Fallback: create a generic lambda string
func_source = "lambda t, **kwargs: 0" # Default fallback
Comment thread
bennthomsen marked this conversation as resolved.
Outdated

desc[segkey]["arguments"] = {
"func_type": "lambda",
"func_source": func_source,
"kwargs": kwargs_dict,
}
else:
sig = signature(self._funlist[sn])
desc[segkey]["arguments"] = dict(
Expand All @@ -275,6 +331,7 @@ def description(self):
desc["marker2_abs"] = self.marker2
desc["marker1_rel"] = self._segmark1
desc["marker2_rel"] = self._segmark2
desc["SR"] = self._SR

return desc

Expand Down Expand Up @@ -312,7 +369,76 @@ def blueprint_from_description(cls, blue_dict):
if seg_dict["function"] == "waituntil":
arguments = blue_dict[seg]["arguments"].values()
arguments = (list(arguments)[0][0],)
bp_seg.insertSegment(i, "waituntil", arguments)
bp_seg.insertSegment(i, "waituntil", arguments, name=seg_dict["name"])
elif seg_dict["function"] == "function PulseAtoms.arb_func":
# Special handling for arb_func reconstruction
args_dict = blue_dict[seg]["arguments"]

if args_dict.get("func_type") == "lambda":
# Reconstruct lambda function
func_source = args_dict["func_source"]
try:
# Create lambda function from source
func_obj = eval(func_source)
except (SyntaxError, NameError) as e:
# Fallback: create a zero function
print(
f"Warning: Could not reconstruct lambda function '{func_source}'. Using zero function. Error: {e}"
)

def zero_function(t, **kwargs):
return 0

func_obj = zero_function

kwargs_dict = args_dict["kwargs"]
arguments = (func_obj, kwargs_dict)
elif args_dict.get("func_type") == "named_function":
# Reconstruct named function
func_name = args_dict["func_name"]
func_source = args_dict.get("func_source")
kwargs_dict = args_dict["kwargs"]

# Try to reconstruct from source first
func_obj = None
if func_source:
try:
# Execute the function source in a local namespace
local_ns = {}
exec(func_source, globals(), local_ns)
Comment thread
bennthomsen marked this conversation as resolved.
Outdated
if func_name in local_ns:
func_obj = local_ns[func_name]
except Exception as e:
print(
Comment thread
bennthomsen marked this conversation as resolved.
Outdated
f"Warning: Could not reconstruct named function '{func_name}' from source. Error: {e}"
)

# Fallback: try to find function in globals
if func_obj is None:
try:
func_obj = globals()[func_name]
except KeyError:
print(
f"Warning: Could not find function '{func_name}' in globals. Using zero function."
)

def zero_function(t, **kwargs):
return 0

func_obj = zero_function

arguments = (func_obj, kwargs_dict)
else:
# Legacy format or fallback
arguments = tuple(blue_dict[seg]["arguments"].values())

bp_seg.insertSegment(
i,
knowfunctions[seg_dict["function"]],
arguments,
name=re.sub(r"\d", "", seg_dict["name"]),
dur=seg_dict["durations"],
)
else:
arguments = tuple(blue_dict[seg]["arguments"].values())
bp_seg.insertSegment(
Expand All @@ -329,6 +455,8 @@ def blueprint_from_description(cls, blue_dict):
listmarker2 = blue_dict["marker2_rel"]
bp_sum._segmark1 = [tuple(mark) for mark in listmarker1]
bp_sum._segmark2 = [tuple(mark) for mark in listmarker2]
if "SR" in blue_dict:
bp_sum._SR = blue_dict["SR"]
return bp_sum

@classmethod
Expand Down Expand Up @@ -664,7 +792,6 @@ def insertSegment(self, pos, func, args=(), dur=None, name=None, durs=None):

if pos < -1:
raise ValueError("Position must be strictly larger than -1")

if name is None or name == "":
if func == "waituntil":
name = "waituntil"
Expand All @@ -674,7 +801,6 @@ def insertSegment(self, pos, func, args=(), dur=None, name=None, durs=None):
if len(name) > 0:
if name[-1].isdigit():
raise ValueError("Segment name must not end in a number")

if pos == -1:
self._namelist.append(name)
self._namelist = self._make_names_unique(self._namelist)
Expand Down
Loading
Loading