BinaryCIF Auto Data Type Detection Pipeline#53
Conversation
yusufm99
left a comment
There was a problem hiding this comment.
I left the comments with issues to be addressed
…tion tests alongside dictionaryApi
| # Auto-detect (dictionary-free) outputs - kept distinct from the dictionary-driven | ||
| # outputs above so the two tests never clobber each other's files. | ||
| self.__testBcifAutoOutput = os.path.join(self.__pathOutputDir, "1bna-autoDetect-generated.bcif") | ||
| self.__testBcifAutoTranslated = os.path.join(self.__pathOutputDir, "1bna-autoDetect-generated-translated.bcif") |
There was a problem hiding this comment.
Line 91 and 92
Two new output paths added for auto detect: __testBcifAutoOutput and __testBcifAutoTranslated, kept separate from the dictionary-driven output paths.
| tcL.append(tc) | ||
| # | ||
| bcw = BinaryCifWriter(self.__dApi, storeStringsAsBytes=storeStringsAsBytes, applyTypes=False, useFloat64=True) | ||
| bcw = BinaryCifWriter(self.__dApi, useAutoDetect=False, storeStringsAsBytes=storeStringsAsBytes, applyTypes=False, useFloat64=True) |
There was a problem hiding this comment.
useAutoDetect=False added to the BinaryCifWriter(...) call (bug fix — This accomodates for the new flag in BinaryCifWriter; without it, useAutoDetect defaults to True and the dictionary-driven type path is silently skipped)
|
|
||
|
|
||
|
|
||
|
|
There was a problem hiding this comment.
Line 143-202:
New method __testSerializeAutoDetect to accomodate and test the new auto detection system.
Mirrors testSerialize()'s structure but feeds the writer raw containers (no DataCategoryTyped pre-cast) with useAutoDetect=True, dictionaryApi=None.
| def testBcifReaderWriter(self): | ||
| self.__testBcifToCif(self.__pathRcsbBcifGzip, self.__pathOutputRcsbBcifTranslated) | ||
| self.__testCifToBcif(self.__pathPdbxDataFile, self.__pathOutputPdbxBcif, self.__pathOutputPdbxBcifTyped) | ||
| self.__testCifToBcif(self.__pathPdbxDataFile, self.__pathOutputPdbxBcif, self.__pathOutputPdbxBcifTyped, self.__pathOutputPdbxBcifAutoDetect) |
There was a problem hiding this comment.
included the auto detect path. __testCifToBcif(...) call passes the new auto-detect path as a 4th argument.
| self.fail() | ||
|
|
||
| def __testCifToBcif(self, ifp, ofp, ofpTyped): | ||
| def __testCifToBcif(self, ifp, ofp, ofpTyped, ofpAutoDetect): |
There was a problem hiding this comment.
Gained a 4th parameter: ofpAutoDetect
There was a problem hiding this comment.
Rather than adding a new argument of ofpAutoDetect, I would suggest adding useAutoDetect=True and reuse the existing (3rd) argument for the output file path. That would let you re-use most of this function without duplicating code blocks. You would then just call it differently for "dictionary-based" vs "autodetect based tests, e.g.:
def testBcifReaderWriterDictTypes(self): # replaces existing testBcifReaderWriter()
self.__testBcifToCif(self.__pathRcsbBcifGzip, self.__pathOutputRcsbBcifTranslated)
self.__testCifToBcif(self.__pathPdbxDataFile, self.__pathOutputPdbxBcif, self.__pathOutputPdbxBcifTyped, useAutoDetect=False)
def testBcifReaderWriterAutoDetect(self): # new test for AutoDetect
self.__testBcifToCif(self.__pathRcsbBcifGzip, self.__pathOutputRcsbBcifTranslated)
self.__testCifToBcif(self.__pathPdbxDataFile, self.__pathOutputPdbxBcif, self.__pathOutputPdbxBcifAutoDetect, useAutoDetect=True)| if not matchOk: | ||
| logger.error("Category and attribute translation mismatch %r %r: %r (cif) vs. %r (bcif, auto-detect)", cat, attr, cCifAttrTypedL, cBcifAttrTypedL) | ||
| ok = False | ||
| self.assertTrue(ok) |
There was a problem hiding this comment.
Line 333-364
New read-back + comparison block to accommodate the auto detect system.
Reads ofpAutoDetect back, checks block counts, then runs the same type-coercing comparison loop used for the dictionary-typed scenario (duplicated inline rather than factored into a shared helper) against the auto-detected result. Error log message tagged "(bcif, auto-detect)" to distinguish it from a dictionary-path failure.
There was a problem hiding this comment.
Pull request overview
This PR integrates a dictionary-free, single-pass type detection pipeline into the BinaryCIF (BCIF) writer so BCIF column typing can be inferred directly from data (with forced attribute overrides), while retaining the legacy dictionary-driven mode when explicitly selected.
Changes:
- Added
bcif_type_detector.pywithColumnProfileandclassify_column()for column type inference. - Unified dictionary-based and auto-detect type resolution inside
BinaryCifWriter, and plumbeduseAutoDetectthroughIoAdapterPy.writeFile(). - Expanded tests to exercise BCIF writing/round-tripping in auto-detect mode.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| mmcif/io/bcif_type_detector.py | New single-pass column classifier for int/float/string inference. |
| mmcif/io/BinaryCifWriter.py | Adds useAutoDetect, forced-type overrides, and auto-mode casting at encode boundary. |
| mmcif/io/IoAdapterPy.py | Adds useAutoDetect argument and routes BCIF writes through unified writer. |
| mmcif/tests/testIoAdapterPy.py | Adds an auto-detect BCIF write/read/compare path. |
| mmcif/tests/testBinaryCifWriter.py | Adds an auto-detect serialize test and adjusts writer instantiation for dictionary mode. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ''' | ||
| Note: Not using the __same() here because the auto-detect method does not make use of the DataCategoryTyped class. | ||
| I works on raw sting values of the attrbites and then assigns a data type to the column based on the values. | ||
| The __same() method is comparing the attributes of the DataCategoryTyped class which is not used in this test. | ||
| ''' | ||
|
|
||
| #castedContainerList = self.__castForAutoDetect(containerList) | ||
| #self.assertTrue(self.__same(containerList[0], cL[0])) |
There was a problem hiding this comment.
@hriday1136 I actually like the Co-pilot comment here. Can you fix the typos in your single-quoted comment, and convert them to just # prefaced comments? (Also, please delete the commented out/unused code).
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
hriday1136
left a comment
There was a problem hiding this comment.
Worked on all relevant comments. Ready to view.
piehld
left a comment
There was a problem hiding this comment.
Thank you @hriday1136! This is outstanding work!
I've left a handful of comments/suggestions below. Please have a look when you get a chance.
|
|
||
| The three-way col_type result ("int" | "float" | "str") is a direct | ||
| replacement for the dictionaryApi.getTypeCode() → getPdbxItemType() chain | ||
| used in BinaryCifWrite.py. The additional profile fields (int_width, |
There was a problem hiding this comment.
| used in BinaryCifWrite.py. The additional profile fields (int_width, | |
| used in BinaryCifWriter.py. The additional profile fields (int_width, |
| """ | ||
| All classification results for a single column. | ||
|
|
||
| Fields used directly by BinaryCifWrite.py |
There was a problem hiding this comment.
| Fields used directly by BinaryCifWrite.py | |
| Fields used directly by BinaryCifWriter.py |
| # DataCategoryTyped pre-casting is only applied when a | ||
| # dictionaryApi is available — auto-detection works on raw | ||
| # string values and does not require pre-casting. | ||
| if self.__applyTypes and not self.__useAutoDetect: |
There was a problem hiding this comment.
I'd suggest swapping the order of these, since by default the first will always return True while the second returns False, so we can escape this conditional a split millisecond faster if it hits the False first (i.e., it won't bother to check the second conditional).
| if self.__applyTypes and not self.__useAutoDetect: | |
| if not self.__useAutoDetect and self.__applyTypes: |
| if not self.__useAutoDetect: | ||
| cifDataType = self.__dApi.getTypeCode(dObj.getName(), atName) | ||
| if cifDataType is None: | ||
| dataType = "string" | ||
| if not self.__ignoreCastErrors: | ||
| logger.warning( | ||
| "Undefined type for category %s attribute %s - Will treat as string", | ||
| dObj.getName(), | ||
| atName, | ||
| ) | ||
| else: | ||
| dataType = self.__dch.getPdbxItemType(cifDataType) | ||
| else: | ||
| dataType = self.__dch.getPdbxItemType(cifDataType) | ||
| # dataType = "integer" if "int" in cifDataType else "float" if cifPrimitiveType == "numb" else "string" | ||
| forcedType = self.__getForcedAttributeType(dObj, atName) | ||
| if forcedType is not None: | ||
| logger.debug( | ||
| "Forced type override applied for %s.%s -> %s", | ||
| dObj.getName(), | ||
| atName, | ||
| forcedType, | ||
| ) | ||
| dataType = forcedType | ||
| else: | ||
| profile = classify_column(colDataList) | ||
| typeMap = {"int": "integer", "float": "float", "str": "string"} | ||
| dataType = typeMap[profile.col_type] | ||
|
|
||
| if profile.value_count == 0 and self.__dApi is not None: | ||
| cifDataType = self.__dApi.getTypeCode(dObj.getName(), atName) | ||
| if cifDataType is not None: | ||
| dataType = self.__dch.getPdbxItemType(cifDataType) | ||
| logger.debug( | ||
| "Empty/all-sentinel column %s.%s - using dictionary type %r", | ||
| dObj.getName(), | ||
| atName, | ||
| dataType, | ||
| ) |
There was a problem hiding this comment.
Similar to my comment earlier, I'd suggest swapping the order of this if/else conditional—i.e., start with assumption that AutoDetect is True (if self.__useAutoDetect:)—since that will be the default.
|
|
||
| return None | ||
|
|
||
| def __getAttributeType(self, dObj, atName, colDataList): |
There was a problem hiding this comment.
I think you only need to pass the catName here, not the entire dObj:
| def __getAttributeType(self, dObj, atName, colDataList): | |
| def __getAttributeType(self, catName, atName, colDataList): |
Then, replace occurrences of dObj.getName() with catName (both here and in downstream calls).
| typeMap = {"int": "integer", "float": "float", "str": "string"} | ||
| dataType = typeMap[profile.col_type] | ||
|
|
||
| if profile.value_count == 0 and self.__dApi is not None: |
There was a problem hiding this comment.
What is the purpose of this bit? Is it to use the dictionary API data type if all the values are sentinels? If so, I don't think we should bother with this, and instead just default to type "string" (which you're already doing). I can't imagine a situation where the type can't be determined by your script, but somehow is defined in the dictionary.
So basically, if useAudoDetect is True, then we would never use the dictionary even if provided.
| self.fail() | ||
|
|
||
| def __testCifToBcif(self, ifp, ofp, ofpTyped): | ||
| def __testCifToBcif(self, ifp, ofp, ofpTyped, ofpAutoDetect): |
There was a problem hiding this comment.
Rather than adding a new argument of ofpAutoDetect, I would suggest adding useAutoDetect=True and reuse the existing (3rd) argument for the output file path. That would let you re-use most of this function without duplicating code blocks. You would then just call it differently for "dictionary-based" vs "autodetect based tests, e.g.:
def testBcifReaderWriterDictTypes(self): # replaces existing testBcifReaderWriter()
self.__testBcifToCif(self.__pathRcsbBcifGzip, self.__pathOutputRcsbBcifTranslated)
self.__testCifToBcif(self.__pathPdbxDataFile, self.__pathOutputPdbxBcif, self.__pathOutputPdbxBcifTyped, useAutoDetect=False)
def testBcifReaderWriterAutoDetect(self): # new test for AutoDetect
self.__testBcifToCif(self.__pathRcsbBcifGzip, self.__pathOutputRcsbBcifTranslated)
self.__testCifToBcif(self.__pathPdbxDataFile, self.__pathOutputPdbxBcif, self.__pathOutputPdbxBcifAutoDetect, useAutoDetect=True)| # Updates: | ||
| # - dictionaryApi type resolution replaced with auto-detection via | ||
| # bcif_type_detector.classify_column(). | ||
| # - dictionaryApi parameter is now optional (defaults to None). | ||
| # If None, auto-detection is used for all columns. | ||
| # If supplied, it is used only as a fallback for all-sentinel/empty columns. | ||
| # - DataCategoryTyped pre-casting is skipped when dictionaryApi is None. | ||
| # - __encodeColumnData() casts raw string values to int/float before encoding. | ||
| # - __getAttributeType() uses classify_column() as primary type resolver. | ||
| # - _FORCE_STRING_ATTRS overrides auto-detection for char-typed attributes | ||
| # that look numeric (e.g. _audit_conform.dict_version = "5.281"). |
There was a problem hiding this comment.
Can you reformat this to follow the same structure I proposed for Yusuf's PR (#52 (comment))? Just add a new line with your initials, then put these bullets below that.
piehld
left a comment
There was a problem hiding this comment.
Added a few more comments, and back-pedaled on a coupled old ones.
| # Key format: "category.attribute" (category name without leading underscore, | ||
| # both lowercased for case-insensitive matching). |
There was a problem hiding this comment.
I would actually prefer you retain the casing in the _FORCE_...ATTRS lists below, as well as the leading underscore (e.g., _atom_site.group_PDB), since things can get confusing when we force .lower() them during comparison. Also, doing this will let us merge the predefined items in Yusuf's code with yours more easily.
Apologies that reverting them will require you to re-check each attribute in the dictionary (mmcif.wwpdb.org).
| This avoids scanning the full column with classify_column() | ||
| when the attribute type is already known. | ||
| """ | ||
| atKey = "%s.%s" % (dObj.getName().lower(), atName.lower()) |
There was a problem hiding this comment.
Please do not apply the .lower() transformation here, and include the leading underscore.
| atKey = "%s.%s" % (dObj.getName().lower(), atName.lower()) | |
| atKey = "_%s.%s" % (catName, atName) |
| # bool must be checked before int — bool is a subclass of int in Python | ||
| if isinstance(v, bool): | ||
| all_int = all_float = False | ||
| type_decided = True | ||
| continue |
There was a problem hiding this comment.
(If the answer is no, then we should be able to remove this part)
BinaryCIF Auto-Detection Encoding Pipeline
An optional dictionary-free column-type resolution layer integrated directly into
py-mmcif's existing BinaryCIF (BCIF) writer. The writer can now use either the originalDictionaryApi-driven type lookup or a single-pass data-type detector, removing the PDBx/mmCIF dictionary dependency when auto-detection is enabled while preserving the original dictionary-based behavior.Documents every feature added on top of upstream
rcsb/py-mmcif(baseline v1.1.1), how each works, and the implementation decisions behind them.Overview
BCIF encoding requires every column to be typed
integer,float, orstringso the right encoder cascade is selected. Upstreampy-mmcifgets those types from the loaded PDBx/mmCIF dictionary viaDictionaryApi.getTypeCode(). That requires a multi-megabyte dictionary to be loaded, and it silently falls back tostringfor any attribute not in the dictionary — inflating file size for custom or extended CIF categories whose numeric columns get mis-encoded.When enabled, this pipeline infers column types directly from the data in a single pass, with a small override table for the few attributes whose correct type can't be inferred safely from values alone. The same
BinaryCifWriterclass continues to support the original dictionary-based path when auto-detection is disabled.Summary of changes
New module —
bcif_type_detector.pyColumnProfiledataclass holding the full per-column classification result.classify_column()— single-pass, two-phase (type probe + statistics) classifier.isinstance→ char-levelisdigit()→ compiled regex..,?,None) before type analysis.bool-before-intguard; leading-zero protection ("0004"→ string).f32/f64).total == 0branch ordered first to avoid a crash).is_sequential,has_long_runs,unique_ratio, etc.mmcifmodule — independently testable.Modified writer —
BinaryCifWriter.pyBinaryCifWriter; the separateBinaryCifWriter_autoDetect.pyclass is no longer needed.dictionaryApiis optional whenuseAutoDetect=Trueand remains required for dictionary mode.useAutoDetectto select between auto-detection and the original dictionary-driven type lookup in the same writer.DataCategoryTypedpre-casting remains unchanged in dictionary mode and is skipped in auto-detection mode.__encodeColumnData()for auto-detected integer and float columns beforestruct.pack._FORCE_STRING_ATTRS,_FORCE_INTEGER_ATTRS,_FORCE_FLOAT_ATTRS.__getAttributeType()now acceptscolDataListand dispatches to either the original dictionary path or the auto-detection path.Modified adapter —
IoAdapterPy.pyBinaryCifWriter; the separate auto-detect writer import is removed.useAutoDetect=TrueonwriteFile()and passes the flag into the unified writer.useAutoDetect=True(default) instantiatesBinaryCifWriterwithdictionaryApi=Noneand enables auto-detection.useAutoDetect=Falseinstantiates the same writer with the supplieddictionaryApi, preserving the legacy dictionary-driven path.readFile()path remain unchanged.Architecture
bcif_type_detector.py— pure classification logic, nommcifdependency.BinaryCifWriter.py— contains both the original dictionary path and the new auto-detection path, applies overrides, casts raw numeric strings in auto mode, and drives the existing encoder cascade.IoAdapterPy.py— constructs the unified writer and selects the mode throughuseAutoDetect.Component deep-dive
1.
bcif_type_detector.py— the classifierWhat it does. For each column it decides
integer/float/string— the choice upstream got from the dictionary — by inferring it from the data in one pass, while also gathering statistics the writer uses for tighter encoding. The result is aColumnProfile: the primarycol_typeplus advisory fields (int_width,float_prec,col_min/col_max,max_decimals,is_sequential,has_long_runs,unique_ratio,value_count,sentinel_count). Onlycol_typeis required.How it does it. One loop, two concurrent phases:
None,".","?"are counted and skipped before any type logic, so missing markers never pollute the inferred type.all_int/all_floatflags startTrueand get knocked down. Checks are ordered cheapest-first:isinstance(v, bool)(before int, since bool subclasses int) →isinstanceint/float → for strings, a char-levelisdigit()fast path, then the compiled_FLOATregex only if that fails. Once a non-numeric value appears,type_decidedflips and Phase A stops working, but the loop does not break.unique_ratio), the adjacent run-length tracker (has_long_runsat run ≥ 4), and the sequential check for integers (is_sequential). Min/max and max decimals accumulate.Finalization.
total == 0(all sentinels) →int, checked first because the numeric branches dereferencecol_min(stillNone). Elseall_int→ integer with narrowed width;all_float→ float withf32/f64; otherwise → string.The net effect is one O(n) pass that types correctly on custom/extended CIF where the dictionary would silently fall back to string.
2.
BinaryCifWriter.py— the unified writerWhat it does. Writes the
.bciffile using either the original dictionary-based column typing or the new data-driven typing path. In dictionary mode, behavior stays aligned with upstream: values can be pre-cast throughDataCategoryTyped, and types come fromDictionaryApi. In auto-detection mode, the writer resolves types from the column values, casts raw strings to realint/floatobjects only at the encoder boundary, and leaves the encoder math untouched.How it does it.
useAutoDetect.dictionaryApimay beNonein auto mode, but dictionary mode still requires it.serialize()— appliesDataCategoryTypedonly whenapplyTypes=TrueanduseAutoDetect=False. Auto-detected columns remain raw until encoding. The column data is passed to__getAttributeType()so it can classify values when needed.__getAttributeType()— first branches by mode. Dictionary mode retains the originalDictionaryApi.getTypeCode()behavior. Auto mode resolves in a strict four-step order: forced override (O(1), skips the scan) →classify_column()→ dictionary fallback only whenvalue_count == 0and a dictionary is available → MolStar integer hints.__encodeColumnData()— in auto mode, casts each value toint(v)/float(v)according to the resolved type. Sentinels (None,.,?) are deliberately not cast, sogetMask()continues to build the same valid incompleteness mask.Everything below this —
BinaryCifEncoders,TypedArray, and all encoders — remains unchanged, which keeps output spec-compliant and round-trip compatible.3.
IoAdapterPy.py— the adapterWhat it does. A thin routing layer that constructs the unified
BinaryCifWriterand selects dictionary or auto-detection mode for each BCIF write. The dictionary-free path remains the default.How it does it. The separate
BinaryCifWriter_autoDetectimport and writer selection branch are removed.writeFile()keepsuseAutoDetect=True, and thefmt == "bcif"branch now always createsBinaryCifWriter:useAutoDetect=True(default) passesuseAutoDetect=TrueanddictionaryApi=None, preventing a stray dictionary argument from changing theauto-detection path.
useAutoDetect=FalsepassesuseAutoDetect=Falseand the supplieddictionaryApi, reproducing the original dictionary-driven behavior through the same writer class.All existing arguments keep their meaning;
readFile()and themmcifwrite path are untouched. The caller-facing switch remains the same, but the implementation no longer maintains two copies of the BinaryCIF writer.Implementation decisions and pre-defined factors
Three-type vocabulary. The pipeline is constrained to
integer/float/stringbecause BCIF's masked-encoder layer has exactly three entry points (IntArrayMasked,FloatArrayMasked,StringArrayMasked). The detector emits"int"/"float"/"str"and the writer maps them explicitly, keeping the detector independent of the writer's encoder naming.Integer-width boundaries are the exact representable ranges of BCIF's fixed-width types; the narrowest lossless type is chosen so
IntegerPackingworks from the smallest source. Unsigned-first at each width gives non-negative columns an extra bit before stepping up a size.min ≥ 0,max ≤ 255uint8min ≥ -128,max ≤ 127int8min ≥ 0,max ≤ 65535uint16min ≥ -32768,max ≤ 32767int16int32Float cutoff
max_dec ≤ 6 → f32, elsef64. float32 holds ~7 reliable decimal digits; capping at 6 places stays inside that envelope, so low-precision columns use the half-size float while higher-precision ones widen — conservative by default.Run-length threshold ≥ 4. Four is where RunLength's
(value, count)overhead starts paying off, sohas_long_runssignals the writer to attempt it.Sequential guard
total > 1. A single value is trivially sequential, so the flag is suppressed below two values; floats reset the tracker since the consecutive-integer assumption doesn't apply.Sentinel set
{".", "?"}+None— CIF's two missing-value markers plus programmatic absence. Pinned identically in detector and writer; an earlier inconsistency across mask-building, classification, and casting was a real bug source.Leading zeros force string (
"0004","00.5"): zero-padding is semantically meaningful in CIF IDs and casting would discard it, so correctness wins over the smaller numeric encoding.boolbeforeint: bool subclasses int, so an unguarded int check would swallowTrue/Falseas1/0; the explicit check forces them to string.All-sentinel → integer, checked first. A fully-masked integer column collapses under Delta+RunLength and decodes identically regardless of declared type, so it's the cheapest correct answer. The branch runs first because the numeric branches dereference
col_min(Nonehere).Override tables (
_FORCE_STRING_ATTRS,_FORCE_INTEGER_ATTRS,_FORCE_FLOAT_ATTRS) encode the minimum dictionary knowledge values can't express: char fields that look numeric (_audit_conform.dict_version = "5.281",atom_site.type_symbol,label_*_id), ID/sequence integers (atom_site.id,auth_seq_id), and coordinate floats (Cartn_x/y/z,occupancy,B_iso_or_equiv). Keys are stored lowercased for case-insensitive matching and looked up first, so known attributes return in O(1) and skip the scan.Auto-detection resolution order (override → detect → dictionary-on-empty → MolStar) reflects priority: overrides are ground truth; detection is the general case; an optional dictionary is used only when data inference is impossible; MolStar hints preserve exact upstream behavior. When
useAutoDetect=False, the writer uses the original dictionary lookup directly instead of this sequence.Casting at the encoder boundary. In auto-detection mode, pre-casting is skipped, so values can arrive as strings while
struct.packneeds real numerics. Casting therefore happens in__encodeColumnData— sentinels excluded so the mask stays valid. Dictionary mode retains the originalDataCategoryTypedbehavior. The encoder cascade is unchanged, which preserves spec-compliant, round-trip-compatible output.Config flags.
useFloat64=Trueglobally overrides per-columnfloat_prec(safety posture: no precision loss), makingf32unreachable.storeStringsAsBytes=Falseis the required RCSB default — deviating causes msgpack key mismatches.Type-resolution order
When
useAutoDetect=True:_FORCE_*_ATTRS. O(1), skips the column scan.classify_column(colDataList).value_count == 0and a dictionary exists.When
useAutoDetect=False, the writer follows the original dictionary-based typelookup and
DataCategoryTypedpath.Known behaviors and caveats
useFloat64=Truemakes thef32path unreachable (intentional safety posture).Statistical Significance for each feature
Not Forcing Attributes vs Forcing Attributes
Dictionary vs Auto DataType Detection System
Conclusive Summary
This work integrates an automatic column-type detection system directly into the existing BinaryCIF writer. The unified writer can determine each column's type from the actual data without loading the PDBx/mmCIF dictionary, while still retaining the original dictionary-based path. This avoids maintaining separate writer implementations and keeps the feature backward compatible.
The main features added are:
BinaryCifWriter, removing the duplicated auto-detect writer implementation.useAutoDetect=True, which remains the default adapter behavior.useAutoDetect=False) uses the original dictionary-based method through the same writer class.Together, these changes make the dictionary optional, improve type accuracy on files the dictionary does not fully cover, remove duplicate writer code, and keep output size competitive without disrupting existing workflows.