diff --git a/docs/_toc.yml b/docs/_toc.yml
index 7410c2569..6b81cfa82 100644
--- a/docs/_toc.yml
+++ b/docs/_toc.yml
@@ -27,6 +27,7 @@ parts:
- file: explanations/templates.md
- file: explanations/shapes-and-templates.md
- file: explanations/shacl_to_sparql.md
+ - file: explanations/point-label-parsing.md
- caption: Appendix
chapters:
- file: bibliography.md
diff --git a/docs/explanations/point-label-parsing.md b/docs/explanations/point-label-parsing.md
new file mode 100644
index 000000000..5e969f9b0
--- /dev/null
+++ b/docs/explanations/point-label-parsing.md
@@ -0,0 +1,210 @@
+# Point Label Parsing
+
+The purpose of this explanation is to describe the framework for defining point label parsing rules and provide examples of how to use it.
+
+One common source of building metadata are the "point labels" used in building management systems to label or tag the input and output data points with some human-readable description.
+It is often useful to extract structured information from these labels to help with constructing a semantic model of the building.
+
+BuildingMOTIF provides a framework for defining point label naming conventions and parsing them into structured data.
+The output of this process is a set of typed Token objects that can be input into a "Semantic Graph Synthesis" process to generate a semantic model of the building.
+
+```{admonition} Semantic Graph Synthesis
+This feature is coming soon! This label parsing framework is just part of the larger BuildingMOTIF toolkit for generating semantic models of buildings.
+```
+
+## Background
+
+The point label parsing framework in BuildingMOTIF is based on the concept of "parser combinators".
+Parser combinators are a way of defining parsers by combining smaller parsers together.
+In BuildingMOTIF, the "combinators" are defined as Python functions that take a string as input and return a list of TokenResults.
+These combinators can be combined together to create more complex parsers.
+
+
+Here is a short example:
+
+```python
+def parse_ahu_label(label: str) -> List[TokenResult]:
+ return sequence(
+ string("AHU", Constant(BRICK.Air_Handling_Unit)),
+ string("-", Delimiter),
+ regex(r"\d+", Identifier)
+ )(label)
+```
+
+This defines a parser that matches strings like "AHU-1" or "AHU-237" and returns a list of `Token`s.
+The `sequence` combinator combines the three parsers together, and the `string` and `regex` combinators match specific strings or regular expressions.
+Using parser combinators in this way allows you to define complex parsing rules in a concise and readable way.
+
+The example output of the `parse_ahu_label` function might look like this:
+
+```python
+parse_ahu_label("AHU-1")
+# [TokenResult(value='AHU', token=Constant(value=rdflib.term.URIRef('https://brickschema.org/schema/Brick#Air_Handling_Unit')), length=3, error=None, id=None),
+# TokenResult(value='-', token=Delimiter(value='-'), length=1, error=None, id=None),
+# TokenResult(value='1', token=Identifier(value='1'), length=1, error=None, id=None)]
+
+parse_ahu_label("AH-1")
+# [TokenResult(value=None, token=Null(value=None), length=0, error='Expected AHU, got AH-', id=None)]
+```
+
+## Parser Combinators
+
+The `buildingmotif.label_parsing.combinators` module provides a set of parser combinators for defining point label parsing rules.
+Here are some of the most commonly used combinators:
+
+- `string`: Matches a specific string and returns a `Token` with a constant value.
+- `regex`: Matches a regular expression and returns a `Token` with the matched value.
+- `choice`: Matches one of a list of parsers. Uses the first one that matches.
+- `sequence`: Matches a sequence of parsers and returns a list of `Token`s.
+- `constant`: Returns a `Token` with a constant value. Does not consume any input.
+- `many`: Matches zero or more occurrences of a parser.
+- `maybe`: Matches zero or one occurrence of a parser.
+- `until`: Matches a parser until another parser is matched.
+
+
+### Defining New Combinators
+
+These are all just Python functions, so you can define your own combinators as needed.
+
+```python
+delimiters = regex(r"[._:/\- ]", Delimiter)
+identifier = regex(r"[a-zA-Z0-9]+", Identifier)
+named_equip = sequence(equip_abbreviations, maybe(delimiters), identifier)
+named_point = sequence(point_abbreviations, maybe(delimiters), identifier)
+```
+
+More generally, a combinator is any function that takes a string as input and returns a list of `TokenResult`s.
+The methods above (`regex`, `sequence`, `delimiters`) are functions that *return* a combinator as an argument.
+
+### Abbreviations
+
+Abbreviations are a common feature of point labels.
+Strings like "AHU" for "Air Handling Unit" or "VAV" for "Variable Air Volume" are often used to save space on labels.
+You can use the `abbreviations` combinator to define a set of abbreviations and automatically expand them in the input string.
+
+We can define a dictionary of abbreviations like this:
+
+```python
+my_abbreviations = {
+ "AHU": BRICK.Air_Handling_Unit,
+ "FCU": BRICK.Fan_Coil_Unit,
+ "VAV": BRICK.Variable_Air_Volume_Box,
+ "CRAC": BRICK.Computer_Room_Air_Conditioner,
+ "HX": BRICK.Heat_Exchanger,
+ "PMP": BRICK.Pump,
+ "RVAV": BRICK.Variable_Air_Volume_Box_With_Reheat,
+ "HP": BRICK.Heat_Pump,
+ "RTU": BRICK.Rooftop_Unit,
+ "DMP": BRICK.Damper,
+ "STS": BRICK.Status,
+ "VLV": BRICK.Valve,
+ "CHVLV": BRICK.Chilled_Water_Valve,
+ "HWVLV": BRICK.Hot_Water_Valve,
+ "VFD": BRICK.Variable_Frequency_Drive,
+ "CT": BRICK.Cooling_Tower,
+ "MAU": BRICK.Makeup_Air_Unit,
+ "R": BRICK.Room,
+}
+
+my_abbreviations_parser = abbreviations(my_abbreviations)
+```
+
+Then we can use `my_abbreviations_parser` in our label parsing rules to automatically expand abbreviations.
+Note how the key of the `my_abbreviations` dictionary is the abbreviation and the value is the RDF Brick class that the abbreviation expands to.
+
+To expand our earlier example to work for other abbreviations, we can rewrite the parser like this:
+
+```python
+def parse_label(label: str) -> List[TokenResult]:
+ return sequence(
+ my_abbreviations_parser,
+ string("-", Delimiter),
+ regex(r"\d+", Identifier)
+ )(label)
+
+parse_label("AHU-1")
+# [TokenResult(value='AHU', token=Constant(value=rdflib.term.URIRef('https://brickschema.org/schema/Brick#Air_Handling_Unit')), length=3, error=None, id=None),
+# TokenResult(value='-', token=Delimiter(value='-'), length=1, error=None, id=None),
+# TokenResult(value='1', token=Identifier(value='1'), length=1, error=None, id=None)]
+
+parse_label("FCU-1")
+# [TokenResult(value='FCU', token=Constant(value=rdflib.term.URIRef('https://brickschema.org/schema/Brick#Fan_Coil_Unit')), length=3, error=None, id=None),
+# TokenResult(value='-', token=Delimiter(value='-'), length=1, error=None, id=None),
+# TokenResult(value='123', token=Identifier(value='123'), length=3, error=None, id=None)]
+
+parse_label("AH-1")
+# [TokenResult(value=None, token=Null(value=None), length=0, error='Expected
+# AHU, got AH- | Expected FCU, got AH- | Expected VAV, got AH- | Expected CRAC,
+# got AH-3 | Expected HX, got AH | Expected PMP, got AH- | Expected RVAV, got
+# AH-3 | Expected HP, got AH | Expected RTU, got AH- | Expected DMP, got AH- |
+# Expected STS, got AH- | Expected VLV, got AH- | Expected CHVLV, got AH-3 |
+# Expected HWVLV, got AH-3 | Expected VFD, got AH- | Expected CT, got AH |
+# Expected MAU, got AH- | Expected R, got A', id=None)]
+```
+
+### Error Handling
+
+The parser combinators in BuildingMOTIF provide detailed error messages when a parsing rule fails.
+This can be useful for debugging and understanding why a particular label did not match the expected format.
+The error messages include information about what was expected and what was found in the input string.
+
+If any `TokenResult` in the list has an `error` field, it means that the parsing rule failed at that point.
+
+## Example
+
+Consider these point labels:
+
+```
+:BuildingName_02:FCU503_ChwVlvPos
+:BuildingName_02:FCU510_EffOcc
+:BuildingName_02:FCU507_UnoccHtgSpt
+:BuildingName_02:FCU415_UnoccHtgSpt
+:BuildingName_01:FCU203_OccClgSpt
+:BuildingName_02:FCU529_UnoccHtgSpt
+:BuildingName_01:FCU243_EffOcc
+:BuildingName_01:FCU362_ChwVlvPos
+```
+
+We can define a set of parsing rules to extract structured data from these labels.
+This is essentially just an expression of the building point naming convention.
+
+```python
+equip_abbreviations = abbreviations(COMMON_EQUIP_ABBREVIATIONS_BRICK)
+# define our own for Points (specific to this building)
+point_abbreviations = abbreviations({
+ "ChwVlvPos": BRICK.Position_Sensor,
+ "HwVlvPos": BRICK.Position_Sensor,
+ "RoomTmp": BRICK.Air_Temperature_Sensor,
+ "Room_RH": BRICK.Relative_Humidity_Sensor,
+ "UnoccHtgSpt": BRICK.Unoccupied_Air_Temperature_Heating_Setpoint,
+ "OccHtgSpt": BRICK.Occupied_Air_Temperature_Heating_Setpoint,
+ "UnoccClgSpt": BRICK.Unoccupied_Air_Temperature_Cooling_Setpoint,
+ "OccClgSpt": BRICK.Occupied_Air_Temperature_Cooling_Setpoint,
+ "SaTmp": BRICK.Supply_Air_Temperature_Sensor,
+ "OccCmd": BRICK.Occupancy_Command,
+ "EffOcc": BRICK.Occupancy_Status,
+})
+
+def custom_parser(target):
+ return sequence(
+ string(":", Delimiter),
+ # regex until the underscore
+ constant(Constant(BRICK.Building)),
+ regex(r"[^_]+", Identifier),
+ string("_", Delimiter),
+ # number for AHU name
+ constant(Constant(BRICK.Air_Handling_Unit)),
+ regex(r"[0-9a-zA-Z]+", Identifier),
+ string(":", Delimiter),
+ # equipment types
+ equip_abbreviations,
+ # equipment ident
+ regex(r"[0-9a-zA-Z]+", Identifier),
+ string("_", Delimiter),
+ maybe(
+ sequence(regex(r"[A-Z]+[0-9]+", Identifier), string("_", Delimiter)),
+ ),
+ # point types
+ point_abbreviations,
+ )(target)
+```